diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5e99b95b9..f06c4a959c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,7 +72,7 @@ jobs: with: node_version: "18" buf_version: "latest" - go_lint_version: "v1.62.2" + go_lint_version: "v1.64.8" core_cache_key: ${{ needs.core.outputs.cache_key }} core_cache_path: ${{ needs.core.outputs.cache_path }} diff --git a/API_DESIGN.md b/API_DESIGN.md index ea37df5a24..11b7766a49 100644 --- a/API_DESIGN.md +++ b/API_DESIGN.md @@ -48,6 +48,52 @@ When creating a new service, start with version `2`, as version `1` is reserved 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/ +### Deprecations + +As a rule of thumb, redundant API methods are deprecated. + +- The proto option `grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation.deprecated` MUST be set to true. +- One or more links to recommended replacement methods MUST be added to the deprecation message as a proto comment above the rpc spec. +- Guidance for switching to the recommended methods for common use cases SHOULD be added as a proto comment above the rpc spec. + +#### Example + +```protobuf +// Delete the user phone +// +// Deprecated: [Update the user's phone field](apis/resources/user_service_v2/user-service-update-user.api.mdx) to remove the phone number. +// +// Delete the phone number of a user. +rpc RemovePhone(RemovePhoneRequest) returns (RemovePhoneResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/phone" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + responses: { + key: "200" + value: { + description: "OK"; + } + }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } + }; +} +``` + ### 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. @@ -73,6 +119,8 @@ For example, use `organization_id` instead of **org_id** or **resource_owner** f #### Resources and Fields +##### Context information in Requests + 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`. @@ -90,6 +138,65 @@ Only allow providing a context where it is required. The context MUST not be pro 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. +##### Context information in Responses + +When the action of creation, update or deletion of a resource was successful, the returned response has to include the time of the operation and the generated identifiers. +This is achieved through the addition of a timestamp attribute with the operation as a prefix, and the generated information as separate attributes. + +```protobuf +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 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 UpdateProjectGrantResponse { + // The timestamp of the change of the project grant. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteProjectGrantResponse { + // The timestamp of the deletion of the project grant. + // 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 = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} +``` + +##### Global messages + 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. @@ -99,6 +206,10 @@ 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. +On the other hand, types that always follow the same pattern and are used in multiple resources, such as `IDFilter`, `TimestampFilter` or `InIDsFilter` SHOULD be globalized and reused. + +##### Re-using messages + 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 @@ -162,7 +273,7 @@ Additionally, state changes, specific actions or operations that do not fit into 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. +Permissions granted 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). @@ -190,33 +301,54 @@ In case the permission cannot be checked by the API itself, but all requests nee }; ``` -## Pagination +## Listing resources 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 +### Pagination -// 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; +Most listing methods SHOULD use the `PaginationRequest` message to allow the client to specify the limit, offset, and sorting options. +```protobuf +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\"]}}]}"; + }; } ``` -On the corresponding responses the `ListDetails` can be used to return the total count of the resources + +On the corresponding responses the `PaginationResponse` 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. +### Filter method + +All filters in List operations SHOULD provide a method if not already specified by the filters name. +```protobuf +message TargetNameFilter { + // Defines the name of the target to query for. + string target_name = 1 [ + (validate.rules).string = {max_len: 200} + ]; + // Defines which text comparison method used for the name query. + zitadel.filter.v2beta.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} +``` + ## Error Handling The API returns machine-readable errors in the response body. This includes a status code, an error code and possibly diff --git a/README.md b/README.md index 285e50964c..3d33e20e57 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ Use [Console](https://zitadel.com/docs/guides/manage/console/overview) or our [A ### 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)] +![New Login Showcase](https://github.com/user-attachments/assets/cb5c5212-128b-4dc9-b11d-cabfd3f73e26) ## Security diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 8482ccec9f..7bb44b743f 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -312,6 +312,16 @@ Caches: AddSource: true Formatter: Format: text + # Federated logouts store the information needed to handle federated logout and their state transfer + FederatedLogouts: + Connector: "postgres" + MaxAge: 1h + LastUseAge: 10m + Log: + Level: error + AddSource: true + Formatter: + Format: text Machine: # Cloud-hosted VMs need to specify their metadata endpoint so that the machine can be uniquely identified. @@ -387,7 +397,8 @@ Projections: org_domain_verified_fields: TransactionDuration: 0s BulkLimit: 2000 - + execution_handler: + BulkLimit: 10 # 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 @@ -607,14 +618,14 @@ 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" + # - 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: @@ -652,7 +663,7 @@ SystemDefaults: # or cost are automatically re-hashed using this config, # upon password validation or update. Hasher: - # Supported algorithms: "argon2i", "argon2id", "bcrypt", "scrypt", "pbkdf2" + # Supported algorithms: "argon2i", "argon2id", "bcrypt", "scrypt", "pbkdf2", "sha2" # Depending on the algorithm, different configuration options take effect. Algorithm: bcrypt # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM # Cost takes effect for the algorithms bcrypt and scrypt @@ -663,10 +674,11 @@ SystemDefaults: Memory: 32768 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_MEMORY # Threads takes effect for the algorithms argon2i and argon2id Threads: 4 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_THREADS - # Rounds takes effect for the algorithm pbkdf2 + # Rounds takes effect for the algorithm pbkdf2 and sha2 Rounds: 290000 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ROUNDS - # Hash takes effect for the algorithm pbkdf2 - # Can be "sha1", "sha224", "sha256", "sha384" or "sha512" + # Hash takes effect for the algorithm pbkdf2 and sha2 + # Can be "sha1", "sha224", "sha256", "sha384" or "sha512" for pbkdf2 + # Can be "sha256" or "sha512" for sha2 Hash: sha256 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_HASH # Verifiers enable the possibility of verifying @@ -688,6 +700,8 @@ SystemDefaults: # - "md5" # md5Crypt with salt and password shuffling. # - "md5plain" # md5 digest of a password without salt # - "md5salted" # md5 digest of a salted password + # - "phpass" + # - "sha2" # crypt(3) SHA-256 and SHA-512 # - "scrypt" # - "pbkdf2" # verifier for all pbkdf2 hash modes. SecretHasher: @@ -735,6 +749,9 @@ SystemDefaults: DefaultQueryLimit: 100 # ZITADEL_SYSTEMDEFAULTS_DEFAULTQUERYLIMIT # MaxQueryLimit limits the number of items that can be queried in a single v3 API search request with explicitly passing a limit. MaxQueryLimit: 1000 # ZITADEL_SYSTEMDEFAULTS_MAXQUERYLIMIT + # The maximum duration of the IDP intent lifetime after which the IDP intent expires and can not be retrieved or used anymore. + # Note that this time is measured only after the IdP intent was successful and not after the IDP intent was created. + MaxIdPIntentLifetime: 1h # ZITADEL_SYSTEMDEFAULTS_MAXIDPINTENTLIFETIME Actions: HTTP: @@ -959,16 +976,15 @@ DefaultInstance: EmailTemplate: CjwhZG9jdHlwZSBodG1sPgo8aHRtbCB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiIHhtbG5zOm89InVybjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSI+CjxoZWFkPgogIDx0aXRsZT4KCiAgPC90aXRsZT4KICA8IS0tW2lmICFtc29dPjwhLS0+CiAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJJRT1lZGdlIj4KICA8IS0tPCFbZW5kaWZdLS0+CiAgPG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgiPgogIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSI+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KICAgICNvdXRsb29rIGEgeyBwYWRkaW5nOjA7IH0KICAgIGJvZHkgeyBtYXJnaW46MDtwYWRkaW5nOjA7LXdlYmtpdC10ZXh0LXNpemUtYWRqdXN0OjEwMCU7LW1zLXRleHQtc2l6ZS1hZGp1c3Q6MTAwJTsgfQogICAgdGFibGUsIHRkIHsgYm9yZGVyLWNvbGxhcHNlOmNvbGxhcHNlO21zby10YWJsZS1sc3BhY2U6MHB0O21zby10YWJsZS1yc3BhY2U6MHB0OyB9CiAgICBpbWcgeyBib3JkZXI6MDtoZWlnaHQ6YXV0bztsaW5lLWhlaWdodDoxMDAlOyBvdXRsaW5lOm5vbmU7dGV4dC1kZWNvcmF0aW9uOm5vbmU7LW1zLWludGVycG9sYXRpb24tbW9kZTpiaWN1YmljOyB9CiAgICBwIHsgZGlzcGxheTpibG9jazttYXJnaW46MTNweCAwOyB9CiAgPC9zdHlsZT4KICA8IS0tW2lmIG1zb10+CiAgPHhtbD4KICAgIDxvOk9mZmljZURvY3VtZW50U2V0dGluZ3M+CiAgICAgIDxvOkFsbG93UE5HLz4KICAgICAgPG86UGl4ZWxzUGVySW5jaD45NjwvbzpQaXhlbHNQZXJJbmNoPgogICAgPC9vOk9mZmljZURvY3VtZW50U2V0dGluZ3M+CiAgPC94bWw+CiAgPCFbZW5kaWZdLS0+CiAgPCEtLVtpZiBsdGUgbXNvIDExXT4KICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgLm1qLW91dGxvb2stZ3JvdXAtZml4IHsgd2lkdGg6MTAwJSAhaW1wb3J0YW50OyB9CiAgPC9zdHlsZT4KICA8IVtlbmRpZl0tLT4KCgogIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBAbWVkaWEgb25seSBzY3JlZW4gYW5kIChtaW4td2lkdGg6NDgwcHgpIHsKICAgICAgLm1qLWNvbHVtbi1wZXItMTAwIHsgd2lkdGg6MTAwJSAhaW1wb3J0YW50OyBtYXgtd2lkdGg6IDEwMCU7IH0KICAgICAgLm1qLWNvbHVtbi1wZXItNjAgeyB3aWR0aDo2MCUgIWltcG9ydGFudDsgbWF4LXdpZHRoOiA2MCU7IH0KICAgIH0KICA8L3N0eWxlPgoKCiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCgoKICAgIEBtZWRpYSBvbmx5IHNjcmVlbiBhbmQgKG1heC13aWR0aDo0ODBweCkgewogICAgICB0YWJsZS5tai1mdWxsLXdpZHRoLW1vYmlsZSB7IHdpZHRoOiAxMDAlICFpbXBvcnRhbnQ7IH0KICAgICAgdGQubWotZnVsbC13aWR0aC1tb2JpbGUgeyB3aWR0aDogYXV0byAhaW1wb3J0YW50OyB9CiAgICB9CgogIDwvc3R5bGU+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc2hhZG93IGEgewogICAgYm94LXNoYWRvdzogMHB4IDNweCAxcHggLTJweCByZ2JhKDAsIDAsIDAsIDAuMiksIDBweCAycHggMnB4IDBweCByZ2JhKDAsIDAsIDAsIDAuMTQpLCAwcHggMXB4IDVweCAwcHggcmdiYSgwLCAwLCAwLCAwLjEyKTsKICB9PC9zdHlsZT4KCiAge3tpZiAuRm9udFVSTH19CiAgPHN0eWxlPgogICAgQGZvbnQtZmFjZSB7CiAgICAgIGZvbnQtZmFtaWx5OiAne3suRm9udEZhY2VGYW1pbHl9fSc7CiAgICAgIGZvbnQtc3R5bGU6IG5vcm1hbDsKICAgICAgZm9udC1kaXNwbGF5OiBzd2FwOwogICAgICBzcmM6IHVybCh7ey5Gb250VVJMfX0pOwogICAgfQogIDwvc3R5bGU+CiAge3tlbmR9fQoKPC9oZWFkPgo8Ym9keSBzdHlsZT0id29yZC1zcGFjaW5nOm5vcm1hbDsiPgoKCjxkaXYKICAgICAgICBzdHlsZT0iIgo+CgogIDx0YWJsZQogICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJhY2tncm91bmQ6e3suQmFja2dyb3VuZENvbG9yfX07YmFja2dyb3VuZC1jb2xvcjp7ey5CYWNrZ3JvdW5kQ29sb3J9fTt3aWR0aDoxMDAlO2JvcmRlci1yYWRpdXM6MTZweDsiCiAgPgogICAgPHRib2R5PgogICAgPHRyPgogICAgICA8dGQ+CgoKICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIGNsYXNzPSIiIHN0eWxlPSJ3aWR0aDo4MDBweDsiIHdpZHRoPSI4MDAiID48dHI+PHRkIHN0eWxlPSJsaW5lLWhlaWdodDowcHg7Zm9udC1zaXplOjBweDttc28tbGluZS1oZWlnaHQtcnVsZTpleGFjdGx5OyI+PCFbZW5kaWZdLS0+CgoKICAgICAgICA8ZGl2ICBzdHlsZT0ibWFyZ2luOjBweCBhdXRvO2JvcmRlci1yYWRpdXM6MTZweDttYXgtd2lkdGg6ODAwcHg7Ij4KCiAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7Ym9yZGVyLXJhZGl1czoxNnB4OyIKICAgICAgICAgID4KICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iZGlyZWN0aW9uOmx0cjtmb250LXNpemU6MHB4O3BhZGRpbmc6MjBweCAwO3BhZGRpbmctbGVmdDowO3RleHQtYWxpZ246Y2VudGVyOyIKICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgcm9sZT0icHJlc2VudGF0aW9uIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCI+PHRyPjx0ZCBjbGFzcz0iIiB3aWR0aD0iODAwcHgiID48IVtlbmRpZl0tLT4KCiAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICA8dGQ+CgoKICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgY2xhc3M9IiIgc3R5bGU9IndpZHRoOjgwMHB4OyIgd2lkdGg9IjgwMCIgPjx0cj48dGQgc3R5bGU9ImxpbmUtaGVpZ2h0OjBweDtmb250LXNpemU6MHB4O21zby1saW5lLWhlaWdodC1ydWxlOmV4YWN0bHk7Ij48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgICAgPGRpdiAgc3R5bGU9Im1hcmdpbjowcHggYXV0bzttYXgtd2lkdGg6ODAwcHg7Ij4KCiAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJ3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImRpcmVjdGlvbjpsdHI7Zm9udC1zaXplOjBweDtwYWRkaW5nOjA7dGV4dC1hbGlnbjpjZW50ZXI7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgcm9sZT0icHJlc2VudGF0aW9uIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCI+PHRyPjx0ZCBjbGFzcz0iIiBzdHlsZT0id2lkdGg6ODAwcHg7IiA+PCFbZW5kaWZdLS0+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9Im1qLWNvbHVtbi1wZXItMTAwIG1qLW91dGxvb2stZ3JvdXAtZml4IiBzdHlsZT0iZm9udC1zaXplOjA7bGluZS1oZWlnaHQ6MDt0ZXh0LWFsaWduOmxlZnQ7ZGlzcGxheTppbmxpbmUtYmxvY2s7d2lkdGg6MTAwJTtkaXJlY3Rpb246bHRyOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiA+PHRyPjx0ZCBzdHlsZT0idmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjgwMHB4OyIgPjwhW2VuZGlmXS0tPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzcz0ibWotY29sdW1uLXBlci0xMDAgbWotb3V0bG9vay1ncm91cC1maXgiIHN0eWxlPSJmb250LXNpemU6MHB4O3RleHQtYWxpZ246bGVmdDtkaXJlY3Rpb246bHRyO2Rpc3BsYXk6aW5saW5lLWJsb2NrO3ZlcnRpY2FsLWFsaWduOnRvcDt3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHdpZHRoPSIxMDAlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgIHN0eWxlPSJ2ZXJ0aWNhbC1hbGlnbjp0b3A7cGFkZGluZzowOyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7e2lmIC5Mb2dvVVJMfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iIiB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzo1MHB4IDAgMzBweCAwO3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYm9yZGVyLWNvbGxhcHNlOmNvbGxhcHNlO2JvcmRlci1zcGFjaW5nOjBweDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCAgc3R5bGU9IndpZHRoOjE4MHB4OyI+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGltZwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBoZWlnaHQ9ImF1dG8iIHNyYz0ie3suTG9nb1VSTH19IiBzdHlsZT0iYm9yZGVyOjA7Ym9yZGVyLXJhZGl1czo4cHg7ZGlzcGxheTpibG9jaztvdXRsaW5lOm5vbmU7dGV4dC1kZWNvcmF0aW9uOm5vbmU7aGVpZ2h0OmF1dG87d2lkdGg6MTAwJTtmb250LXNpemU6MTNweDsiIHdpZHRoPSIxODAiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAvPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PC90ZD48L3RyPjwvdGFibGU+PCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PC90ZD48L3RyPjwvdGFibGU+PCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KCgogICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PC90ZD48L3RyPjwvdGFibGU+PCFbZW5kaWZdLS0+CgoKICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PHRyPjx0ZCBjbGFzcz0iIiB3aWR0aD0iODAwcHgiID48IVtlbmRpZl0tLT4KCiAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICA8dGQ+CgoKICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgY2xhc3M9IiIgc3R5bGU9IndpZHRoOjgwMHB4OyIgd2lkdGg9IjgwMCIgPjx0cj48dGQgc3R5bGU9ImxpbmUtaGVpZ2h0OjBweDtmb250LXNpemU6MHB4O21zby1saW5lLWhlaWdodC1ydWxlOmV4YWN0bHk7Ij48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgICAgPGRpdiAgc3R5bGU9Im1hcmdpbjowcHggYXV0bzttYXgtd2lkdGg6ODAwcHg7Ij4KCiAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJ3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImRpcmVjdGlvbjpsdHI7Zm9udC1zaXplOjBweDtwYWRkaW5nOjA7dGV4dC1hbGlnbjpjZW50ZXI7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgcm9sZT0icHJlc2VudGF0aW9uIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCI+PHRyPjx0ZCBjbGFzcz0iIiBzdHlsZT0idmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjQ4MHB4OyIgPjwhW2VuZGlmXS0tPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzPSJtai1jb2x1bW4tcGVyLTYwIG1qLW91dGxvb2stZ3JvdXAtZml4IiBzdHlsZT0iZm9udC1zaXplOjBweDt0ZXh0LWFsaWduOmxlZnQ7ZGlyZWN0aW9uOmx0cjtkaXNwbGF5OmlubGluZS1ibG9jazt2ZXJ0aWNhbC1hbGlnbjp0b3A7d2lkdGg6MTAwJTsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCAgc3R5bGU9InZlcnRpY2FsLWFsaWduOnRvcDtwYWRkaW5nOjA7Ij4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iIiB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBzdHlsZT0iZm9udC1zaXplOjBweDtwYWRkaW5nOjEwcHggMjVweDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0eWxlPSJmb250LWZhbWlseTp7ey5Gb250RmFtaWx5fX07Zm9udC1zaXplOjI0cHg7Zm9udC13ZWlnaHQ6NTAwO2xpbmUtaGVpZ2h0OjE7dGV4dC1hbGlnbjpjZW50ZXI7Y29sb3I6e3suRm9udENvbG9yfX07IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID57ey5HcmVldGluZ319PC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MTBweCAyNXB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImZvbnQtZmFtaWx5Ont7LkZvbnRGYW1pbHl9fTtmb250LXNpemU6MTZweDtmb250LXdlaWdodDpsaWdodDtsaW5lLWhlaWdodDoxLjU7dGV4dC1hbGlnbjpjZW50ZXI7Y29sb3I6e3suRm9udENvbG9yfX07IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID57ey5UZXh0fX08L2Rpdj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgoKCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIHZlcnRpY2FsLWFsaWduPSJtaWRkbGUiIGNsYXNzPSJzaGFkb3ciIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MTBweCAyNXB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYm9yZGVyLWNvbGxhcHNlOnNlcGFyYXRlO2xpbmUtaGVpZ2h0OjEwMCU7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYmdjb2xvcj0ie3suUHJpbWFyeUNvbG9yfX0iIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJvcmRlcjpub25lO2JvcmRlci1yYWRpdXM6NnB4O2N1cnNvcjphdXRvO21zby1wYWRkaW5nLWFsdDoxMHB4IDI1cHg7YmFja2dyb3VuZDp7ey5QcmltYXJ5Q29sb3J9fTsiIHZhbGlnbj0ibWlkZGxlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGEKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGhyZWY9Int7LlVSTH19IiByZWw9Im5vb3BlbmVyIG5vcmVmZXJyZXIgbm90cmFjayIgc3R5bGU9ImRpc3BsYXk6aW5saW5lLWJsb2NrO2JhY2tncm91bmQ6e3suUHJpbWFyeUNvbG9yfX07Y29sb3I6I2ZmZmZmZjtmb250LWZhbWlseTp7ey5Gb250RmFtaWx5fX07Zm9udC1zaXplOjE0cHg7Zm9udC13ZWlnaHQ6NTAwO2xpbmUtaGVpZ2h0OjEyMCU7bWFyZ2luOjA7dGV4dC1kZWNvcmF0aW9uOm5vbmU7dGV4dC10cmFuc2Zvcm06bm9uZTtwYWRkaW5nOjEwcHggMjVweDttc28tcGFkZGluZy1hbHQ6MHB4O2JvcmRlci1yYWRpdXM6NnB4OyIgdGFyZ2V0PSJfYmxhbmsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3suQnV0dG9uVGV4dH19CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7e2lmIC5JbmNsdWRlRm9vdGVyfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7cGFkZGluZy10b3A6MjBweDtwYWRkaW5nLXJpZ2h0OjIwcHg7cGFkZGluZy1ib3R0b206MjBweDtwYWRkaW5nLWxlZnQ6MjBweDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxwCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iYm9yZGVyLXRvcDpzb2xpZCAycHggI2RiZGJkYjtmb250LXNpemU6MXB4O21hcmdpbjowcHggYXV0bzt3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9wPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHN0eWxlPSJib3JkZXItdG9wOnNvbGlkIDJweCAjZGJkYmRiO2ZvbnQtc2l6ZToxcHg7bWFyZ2luOjBweCBhdXRvO3dpZHRoOjQ0MHB4OyIgcm9sZT0icHJlc2VudGF0aW9uIiB3aWR0aD0iNDQwcHgiID48dHI+PHRkIHN0eWxlPSJoZWlnaHQ6MDtsaW5lLWhlaWdodDowOyI+ICZuYnNwOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgoKCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxNnB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImZvbnQtZmFtaWx5Ont7LkZvbnRGYW1pbHl9fTtmb250LXNpemU6MTNweDtsaW5lLWhlaWdodDoxO3RleHQtYWxpZ246Y2VudGVyO2NvbG9yOnt7LkZvbnRDb2xvcn19OyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+e3suRm9vdGVyVGV4dH19PC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKCiAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgogICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICA8L2Rpdj4KCgogICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgoKCiAgICAgIDwvdGQ+CiAgICA8L3RyPgogICAgPC90Ym9keT4KICA8L3RhYmxlPgoKPC9kaXY+Cgo8L2JvZHk+CjwvaHRtbD4K # ZITADEL_DEFAULTINSTANCE_EMAILTEMPLATE # WebKeys configures the OIDC token signing keys that are generated when a new instance is created. - # WebKeys are still in alpha, so the config is disabled here. This will prevent generation of keys for now. - # WebKeys: - # Type: "rsa" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_TYPE - # Config: - # Bits: "2048" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_BITS - # Hasher: "sha256" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_HASHER + WebKeys: + Type: "rsa" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_TYPE + Config: + RSABits: "2048" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_BITS + RSAHasher: "sha256" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_HASHER # WebKeys: # Type: "ecdsa" # Config: - # Curve: "P256" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_CURVE + # EllipticCurve: "P256" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_CURVE # Sets the default values for lifetime and expiration for OIDC in each newly created instance # This default can be overwritten for each instance during runtime @@ -1101,7 +1117,26 @@ DefaultInstance: LoginDefaultOrg: true # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINDEFAULTORG # TriggerIntrospectionProjections: false # ZITADEL_DEFAULTINSTANCE_FEATURES_TRIGGERINTROSPECTIONPROJECTIONS # LegacyIntrospection: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LEGACYINTROSPECTION + # UserSchema: false # ZITADEL_DEFAULTINSTANCE_FEATURES_USERSCHEMA + # TokenExchange: false # ZITADEL_DEFAULTINSTANCE_FEATURES_TOKENEXCHANGE + ImprovedPerformance: # ZITADEL_DEFAULTINSTANCE_FEATURES_IMPROVEDPERFORMANCE + # https://github.com/zitadel/zitadel/blob/main/internal/feature/feature.go#L64-L68 + # - 1 # OrgByID + # - 2 # ProjectGrant + # - 3 # Project + # - 4 # UserGrant + # - 5 # OrgDomainVerified + # WebKey: false # ZITADEL_DEFAULTINSTANCE_FEATURES_WEBKEY + # DebugOIDCParentError: false # ZITADEL_DEFAULTINSTANCE_FEATURES_DEBUGOIDCPARENTERROR + # OIDCSingleV1SessionTermination: false # ZITADEL_DEFAULTINSTANCE_FEATURES_OIDCSINGLEV1SESSIONTERMINATION + # DisableUserTokenEvent: false # ZITADEL_DEFAULTINSTANCE_FEATURES_DISABLEUSERTOKENEVENT + # EnableBackChannelLogout: false # ZITADEL_DEFAULTINSTANCE_FEATURES_ENABLEBACKCHANNELLOGOUT + # LoginV2: + # Required: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED + # BaseURI: "" # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI # PermissionCheckV2: false # ZITADEL_DEFAULTINSTANCE_FEATURES_PERMISSIONCHECKV2 + # ConsoleUseV2UserApi: false # ZITADEL_DEFAULTINSTANCE_FEATURES_CONSOLEUSEV2USERAPI + Limits: # AuditLogRetention limits the number of events that can be queried via the events API by their age. # A value of "0s" means that all events are available. diff --git a/cmd/mirror/auth.go b/cmd/mirror/auth.go index 0eba10d05f..3d7ae45bce 100644 --- a/cmd/mirror/auth.go +++ b/cmd/mirror/auth.go @@ -4,6 +4,7 @@ import ( "context" _ "embed" "io" + "strconv" "time" "github.com/jackc/pgx/v5/stdlib" @@ -41,12 +42,16 @@ func copyAuth(ctx context.Context, config *Migration) { logging.OnError(err).Fatal("unable to connect to destination database") defer destClient.Close() - copyAuthRequests(ctx, sourceClient, destClient) + copyAuthRequests(ctx, sourceClient, destClient, config.MaxAuthRequestAge) } -func copyAuthRequests(ctx context.Context, source, dest *database.DB) { +func copyAuthRequests(ctx context.Context, source, dest *database.DB, maxAuthRequestAge time.Duration) { start := time.Now() + logging.Info("creating index on auth.auth_requests.change_date to speed up copy in source database") + _, err := source.ExecContext(ctx, "CREATE INDEX CONCURRENTLY IF NOT EXISTS auth_requests_change_date ON auth.auth_requests (change_date)") + logging.OnError(err).Fatal("unable to create index on auth.auth_requests.change_date") + sourceConn, err := source.Conn(ctx) logging.OnError(err).Fatal("unable to acquire connection") defer sourceConn.Close() @@ -55,9 +60,9 @@ func copyAuthRequests(ctx context.Context, source, dest *database.DB) { errs := make(chan error, 1) go func() { - err = sourceConn.Raw(func(driverConn interface{}) error { + err = sourceConn.Raw(func(driverConn any) error { conn := driverConn.(*stdlib.Conn).Conn() - _, err := conn.PgConn().CopyTo(ctx, w, "COPY (SELECT id, regexp_replace(request::TEXT, '\\\\u0000', '', 'g')::JSON request, code, request_type, creation_date, change_date, instance_id FROM auth.auth_requests "+instanceClause()+") TO STDOUT") + _, err := conn.PgConn().CopyTo(ctx, w, "COPY (SELECT id, regexp_replace(request::TEXT, '\\\\u0000', '', 'g')::JSON request, code, request_type, creation_date, change_date, instance_id FROM auth.auth_requests "+instanceClause()+" AND change_date > NOW() - INTERVAL '"+strconv.FormatFloat(maxAuthRequestAge.Seconds(), 'f', -1, 64)+" seconds') TO STDOUT") w.Close() return err }) @@ -69,7 +74,7 @@ func copyAuthRequests(ctx context.Context, source, dest *database.DB) { defer destConn.Close() var affected int64 - err = destConn.Raw(func(driverConn interface{}) error { + err = destConn.Raw(func(driverConn any) error { conn := driverConn.(*stdlib.Conn).Conn() if shouldReplace { diff --git a/cmd/mirror/config.go b/cmd/mirror/config.go index 9d0113a1d7..5bb19f12de 100644 --- a/cmd/mirror/config.go +++ b/cmd/mirror/config.go @@ -23,7 +23,8 @@ type Migration struct { Source database.Config Destination database.Config - EventBulkSize uint32 + EventBulkSize uint32 + MaxAuthRequestAge time.Duration Log *logging.Config Machine *id.Config diff --git a/cmd/mirror/defaults.yaml b/cmd/mirror/defaults.yaml index 4b42c06534..4d8a0a4eae 100644 --- a/cmd/mirror/defaults.yaml +++ b/cmd/mirror/defaults.yaml @@ -1,61 +1,64 @@ Source: cockroach: - Host: localhost # ZITADEL_DATABASE_COCKROACH_HOST - Port: 26257 # ZITADEL_DATABASE_COCKROACH_PORT - Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE - MaxOpenConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS - MaxIdleConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS - MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME - MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME - Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS + Host: localhost # ZITADEL_SOURCE_COCKROACH_HOST + Port: 26257 # ZITADEL_SOURCE_COCKROACH_PORT + Database: zitadel # ZITADEL_SOURCE_COCKROACH_DATABASE + MaxOpenConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXOPENCONNS + MaxIdleConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXIDLECONNS + MaxConnLifetime: 30m # ZITADEL_SOURCE_COCKROACH_MAXCONNLIFETIME + MaxConnIdleTime: 5m # ZITADEL_SOURCE_COCKROACH_MAXCONNIDLETIME + Options: "" # ZITADEL_SOURCE_COCKROACH_OPTIONS User: - Username: zitadel # ZITADEL_DATABASE_COCKROACH_USER_USERNAME - Password: "" # ZITADEL_DATABASE_COCKROACH_USER_PASSWORD + Username: zitadel # ZITADEL_SOURCE_COCKROACH_USER_USERNAME + Password: "" # ZITADEL_SOURCE_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 + Mode: disable # ZITADEL_SOURCE_COCKROACH_USER_SSL_MODE + RootCert: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_ROOTCERT + Cert: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_CERT + Key: "" # ZITADEL_SOURCE_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: # ZITADEL_SOURCE_POSTGRES_HOST + Port: # ZITADEL_SOURCE_POSTGRES_PORT + Database: # ZITADEL_SOURCE_POSTGRES_DATABASE + MaxOpenConns: # ZITADEL_SOURCE_POSTGRES_MAXOPENCONNS + MaxIdleConns: # ZITADEL_SOURCE_POSTGRES_MAXIDLECONNS + MaxConnLifetime: # ZITADEL_SOURCE_POSTGRES_MAXCONNLIFETIME + MaxConnIdleTime: # ZITADEL_SOURCE_POSTGRES_MAXCONNIDLETIME + Options: # ZITADEL_SOURCE_POSTGRES_OPTIONS User: - Username: # ZITADEL_DATABASE_POSTGRES_USER_USERNAME - Password: # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD + Username: # ZITADEL_SOURCE_POSTGRES_USER_USERNAME + Password: # ZITADEL_SOURCE_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: # ZITADEL_SOURCE_POSTGRES_USER_SSL_MODE + RootCert: # ZITADEL_SOURCE_POSTGRES_USER_SSL_ROOTCERT + Cert: # ZITADEL_SOURCE_POSTGRES_USER_SSL_CERT + Key: # ZITADEL_SOURCE_POSTGRES_USER_SSL_KEY Destination: postgres: - 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 + Host: localhost # ZITADEL_DESTINATION_POSTGRES_HOST + Port: 5432 # ZITADEL_DESTINATION_POSTGRES_PORT + Database: zitadel # ZITADEL_DESTINATION_POSTGRES_DATABASE + MaxOpenConns: 5 # ZITADEL_DESTINATION_POSTGRES_MAXOPENCONNS + MaxIdleConns: 2 # ZITADEL_DESTINATION_POSTGRES_MAXIDLECONNS + MaxConnLifetime: 30m # ZITADEL_DESTINATION_POSTGRES_MAXCONNLIFETIME + MaxConnIdleTime: 5m # ZITADEL_DESTINATION_POSTGRES_MAXCONNIDLETIME + Options: "" # ZITADEL_DESTINATION_POSTGRES_OPTIONS User: - Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME - Password: "" # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD + Username: zitadel # ZITADEL_DESTINATION_POSTGRES_USER_USERNAME + Password: "" # ZITADEL_DESTINATION_POSTGRES_USER_PASSWORD SSL: - 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 + Mode: disable # 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 -EventBulkSize: 10000 +EventBulkSize: 10000 # ZITADEL_EVENTBULKSIZE +# The maximum duration an auth request was last updated before it gets ignored. +# Default is 30 days +MaxAuthRequestAge: 720h # ZITADEL_MAXAUTHREQUESTAGE Projections: # The maximum duration a transaction remains open @@ -64,14 +67,14 @@ Projections: TransactionDuration: 0s # ZITADEL_PROJECTIONS_TRANSACTIONDURATION # turn off scheduler during operation RequeueEvery: 0s - ConcurrentInstances: 7 - EventBulkLimit: 1000 - Customizations: + ConcurrentInstances: 7 # ZITADEL_PROJECTIONS_CONCURRENTINSTANCES + EventBulkLimit: 1000 # ZITADEL_PROJECTIONS_EVENTBULKLIMIT + Customizations: notifications: MaxFailureCount: 1 Eventstore: - MaxRetries: 3 + MaxRetries: 3 # ZITADEL_EVENTSTORE_MAXRETRIES Auth: Spooler: diff --git a/cmd/mirror/event.go b/cmd/mirror/event.go index d513990e10..567fb659a2 100644 --- a/cmd/mirror/event.go +++ b/cmd/mirror/event.go @@ -3,6 +3,8 @@ package mirror import ( "context" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/v2/readmodel" "github.com/zitadel/zitadel/internal/v2/system" @@ -29,7 +31,7 @@ func queryLastSuccessfulMigration(ctx context.Context, destinationES *eventstore return lastSuccess, nil } -func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position float64) error { +func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position decimal.Decimal) error { return destinationES.Push( ctx, eventstore.NewPushIntent( diff --git a/cmd/mirror/event_store.go b/cmd/mirror/event_store.go index 8ce53b150a..be14abe340 100644 --- a/cmd/mirror/event_store.go +++ b/cmd/mirror/event_store.go @@ -8,7 +8,9 @@ import ( "io" "time" + "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/stdlib" + "github.com/shopspring/decimal" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" @@ -69,6 +71,7 @@ func positionQuery(db *db.DB) string { } func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { + logging.Info("starting to copy events") start := time.Now() reader, writer := io.Pipe() @@ -88,7 +91,7 @@ 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") - var maxPosition float64 + var maxPosition decimal.Decimal err = source.QueryRowContext(ctx, func(row *sql.Row) error { return row.Scan(&maxPosition) @@ -100,7 +103,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { logging.WithFields("from", previousMigration.Position, "to", maxPosition).Info("start event migration") nextPos := make(chan bool, 1) - pos := make(chan float64, 1) + pos := make(chan decimal.Decimal, 1) errs := make(chan error, 3) go func() { @@ -130,7 +133,10 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { if err != nil { return zerrors.ThrowUnknownf(err, "MIGRA-KTuSq", "unable to copy events from source during iteration %d", i) } + logging.WithFields("batch_count", i).Info("batch of events copied") + if tag.RowsAffected() < int64(bulkSize) { + logging.WithFields("batch_count", i).Info("last batch of events copied") return nil } @@ -148,7 +154,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { go func() { defer close(pos) for range nextPos { - var position float64 + var position decimal.Decimal err := dest.QueryRowContext( ctx, func(row *sql.Row) error { @@ -171,6 +177,10 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { tag, err := conn.PgConn().CopyFrom(ctx, reader, "COPY eventstore.events2 FROM STDIN") eventCount = tag.RowsAffected() if err != nil { + pgErr := new(pgconn.PgError) + errors.As(err, &pgErr) + + logging.WithError(err).WithField("pg_err_details", pgErr.Detail).Error("unable to copy events into destination") return zerrors.ThrowUnknown(err, "MIGRA-DTHi7", "unable to copy events into destination") } @@ -183,7 +193,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { logging.WithFields("took", time.Since(start), "count", eventCount).Info("events migrated") } -func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position float64, errs <-chan error) { +func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position decimal.Decimal, errs <-chan error) { joinedErrs := make([]error, 0, len(errs)) for err := range errs { joinedErrs = append(joinedErrs, err) @@ -202,6 +212,7 @@ func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, sou } func copyUniqueConstraints(ctx context.Context, source, dest *db.DB) { + logging.Info("starting to copy unique constraints") start := time.Now() reader, writer := io.Pipe() errs := make(chan error, 1) diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index 66b3fb1a26..0ff4356d6f 100644 --- a/cmd/mirror/projections.go +++ b/cmd/mirror/projections.go @@ -3,6 +3,7 @@ package mirror import ( "context" "database/sql" + "fmt" "net/http" "sync" "time" @@ -104,6 +105,7 @@ func projections( config *ProjectionsConfig, masterKey string, ) { + logging.Info("starting to fill projections") start := time.Now() client, err := database.Connect(config.Destination, false) @@ -255,8 +257,10 @@ func projections( go execProjections(ctx, instances, failedInstances, &wg) } - for _, instance := range queryInstanceIDs(ctx, client) { + existingInstances := queryInstanceIDs(ctx, client) + for i, instance := range existingInstances { instances <- instance + logging.WithFields("id", instance, "index", fmt.Sprintf("%d/%d", i, len(existingInstances))).Info("instance queued for projection") } close(instances) wg.Wait() @@ -268,7 +272,7 @@ func projections( func execProjections(ctx context.Context, instances <-chan string, failedInstances chan<- string, wg *sync.WaitGroup) { for instance := range instances { - logging.WithFields("instance", instance).Info("start projections") + logging.WithFields("instance", instance).Info("starting projections") ctx = internal_authz.WithInstanceID(ctx, instance) err := projection.ProjectInstance(ctx) @@ -292,6 +296,13 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc continue } + err = projection.ProjectInstanceFields(ctx) + if err != nil { + logging.WithFields("instance", instance).WithError(err).Info("trigger fields failed") + failedInstances <- instance + continue + } + err = auth_handler.ProjectInstance(ctx) if err != nil { logging.WithFields("instance", instance).WithError(err).Info("trigger auth handler failed") @@ -311,7 +322,7 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc wg.Done() } -// returns the instance configured by flag +// queryInstanceIDs returns the instance configured by flag // or all instances which are not removed func queryInstanceIDs(ctx context.Context, source *database.DB) []string { if len(instanceIDs) > 0 { diff --git a/cmd/mirror/system.go b/cmd/mirror/system.go index 00b48eb491..57eb205436 100644 --- a/cmd/mirror/system.go +++ b/cmd/mirror/system.go @@ -46,6 +46,7 @@ func copySystem(ctx context.Context, config *Migration) { } func copyAssets(ctx context.Context, source, dest *database.DB) { + logging.Info("starting to copy assets") start := time.Now() sourceConn, err := source.Conn(ctx) @@ -70,7 +71,7 @@ func copyAssets(ctx context.Context, source, dest *database.DB) { logging.OnError(err).Fatal("unable to acquire dest connection") defer destConn.Close() - var eventCount int64 + var assetCount int64 err = destConn.Raw(func(driverConn interface{}) error { conn := driverConn.(*stdlib.Conn).Conn() @@ -82,16 +83,17 @@ func copyAssets(ctx context.Context, source, dest *database.DB) { } tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.assets (instance_id, asset_type, resource_owner, name, content_type, data, updated_at) FROM stdin") - eventCount = tag.RowsAffected() + assetCount = tag.RowsAffected() return err }) logging.OnError(err).Fatal("unable to copy assets to destination") logging.OnError(<-errs).Fatal("unable to copy assets from source") - logging.WithFields("took", time.Since(start), "count", eventCount).Info("assets migrated") + logging.WithFields("took", time.Since(start), "count", assetCount).Info("assets migrated") } func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) { + logging.Info("starting to copy encryption keys") start := time.Now() sourceConn, err := source.Conn(ctx) @@ -116,7 +118,7 @@ func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) { logging.OnError(err).Fatal("unable to acquire dest connection") defer destConn.Close() - var eventCount int64 + var keyCount int64 err = destConn.Raw(func(driverConn interface{}) error { conn := driverConn.(*stdlib.Conn).Conn() @@ -128,11 +130,11 @@ func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) { } tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.encryption_keys FROM stdin") - eventCount = tag.RowsAffected() + keyCount = tag.RowsAffected() return err }) logging.OnError(err).Fatal("unable to copy encryption keys to destination") logging.OnError(<-errs).Fatal("unable to copy encryption keys from source") - logging.WithFields("took", time.Since(start), "count", eventCount).Info("encryption keys migrated") + logging.WithFields("took", time.Since(start), "count", keyCount).Info("encryption keys migrated") } diff --git a/cmd/setup/38.go b/cmd/setup/38.go index 0a102c9d12..810510bfdd 100644 --- a/cmd/setup/38.go +++ b/cmd/setup/38.go @@ -15,7 +15,6 @@ var ( type BackChannelLogoutNotificationStart struct { dbClient *database.DB - esClient *eventstore.Eventstore } func (mig *BackChannelLogoutNotificationStart) Execute(ctx context.Context, e eventstore.Event) error { diff --git a/cmd/setup/54.go b/cmd/setup/54.go new file mode 100644 index 0000000000..e4a2e43862 --- /dev/null +++ b/cmd/setup/54.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 54.sql + instancePositionIndex string +) + +type InstancePositionIndex struct { + dbClient *database.DB +} + +func (mig *InstancePositionIndex) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, instancePositionIndex) + return err +} + +func (mig *InstancePositionIndex) String() string { + return "54_instance_position_index_again" +} diff --git a/cmd/setup/54.sql b/cmd/setup/54.sql new file mode 100644 index 0000000000..1dca8c7575 --- /dev/null +++ b/cmd/setup/54.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS es_instance_position ON eventstore.events2 (instance_id, position); diff --git a/cmd/setup/55.go b/cmd/setup/55.go new file mode 100644 index 0000000000..19083515e5 --- /dev/null +++ b/cmd/setup/55.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 55.sql + executionHandlerCurrentState string +) + +type ExecutionHandlerStart struct { + dbClient *database.DB +} + +func (mig *ExecutionHandlerStart) Execute(ctx context.Context, e eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, executionHandlerCurrentState, e.Sequence(), e.CreatedAt(), e.Position()) + return err +} + +func (mig *ExecutionHandlerStart) String() string { + return "55_execution_handler_start" +} diff --git a/cmd/setup/55.sql b/cmd/setup/55.sql new file mode 100644 index 0000000000..60c45d5f94 --- /dev/null +++ b/cmd/setup/55.sql @@ -0,0 +1,22 @@ +INSERT INTO projections.current_states AS cs ( instance_id + , projection_name + , last_updated + , sequence + , event_date + , position + , filter_offset) +SELECT instance_id + , 'projections.execution_handler' + , now() + , $1 + , $2 + , $3 + , 0 +FROM eventstore.events2 AS e +WHERE aggregate_type = 'instance' + AND event_type = 'instance.added' +ON CONFLICT (instance_id, projection_name) DO UPDATE SET last_updated = EXCLUDED.last_updated, + sequence = EXCLUDED.sequence, + event_date = EXCLUDED.event_date, + position = EXCLUDED.position, + filter_offset = EXCLUDED.filter_offset; \ No newline at end of file diff --git a/cmd/setup/56.go b/cmd/setup/56.go new file mode 100644 index 0000000000..72ccb5e6ff --- /dev/null +++ b/cmd/setup/56.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 56.sql + addSAMLFederatedLogout string +) + +type IDPTemplate6SAMLFederatedLogout struct { + dbClient *database.DB +} + +func (mig *IDPTemplate6SAMLFederatedLogout) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, addSAMLFederatedLogout) + return err +} + +func (mig *IDPTemplate6SAMLFederatedLogout) String() string { + return "56_idp_templates6_add_saml_federated_logout" +} diff --git a/cmd/setup/56.sql b/cmd/setup/56.sql new file mode 100644 index 0000000000..f5544eddf5 --- /dev/null +++ b/cmd/setup/56.sql @@ -0,0 +1 @@ +ALTER TABLE IF EXISTS projections.idp_templates6_saml ADD COLUMN IF NOT EXISTS federated_logout_enabled BOOLEAN DEFAULT FALSE; diff --git a/cmd/setup/57.go b/cmd/setup/57.go new file mode 100644 index 0000000000..4c52018f1e --- /dev/null +++ b/cmd/setup/57.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 57.sql + createResourceCounts string +) + +type CreateResourceCounts struct { + dbClient *database.DB +} + +func (mig *CreateResourceCounts) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, createResourceCounts) + return err +} + +func (mig *CreateResourceCounts) String() string { + return "57_create_resource_counts" +} diff --git a/cmd/setup/57.sql b/cmd/setup/57.sql new file mode 100644 index 0000000000..f2f0a40202 --- /dev/null +++ b/cmd/setup/57.sql @@ -0,0 +1,106 @@ +CREATE TABLE IF NOT EXISTS projections.resource_counts +( + id SERIAL PRIMARY KEY, -- allows for easy pagination + instance_id TEXT NOT NULL, + table_name TEXT NOT NULL, -- needed for trigger matching, not in reports + parent_type TEXT NOT NULL, + parent_id TEXT NOT NULL, + resource_name TEXT NOT NULL, -- friendly name for reporting + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + amount INTEGER NOT NULL DEFAULT 1 CHECK (amount >= 0), + + UNIQUE (instance_id, parent_type, parent_id, table_name) +); + +-- count_resource is a trigger function which increases or decreases the count of a resource. +-- When creating the trigger the following required arguments (TG_ARGV) can be passed: +-- 1. The type of the parent +-- 2. The column name of the instance id +-- 3. The column name of the owner id +-- 4. The name of the resource +CREATE OR REPLACE FUNCTION projections.count_resource() + RETURNS trigger + LANGUAGE 'plpgsql' VOLATILE +AS $$ +DECLARE + -- trigger variables + tg_table_name TEXT := TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME; + tg_parent_type TEXT := TG_ARGV[0]; + tg_instance_id_column TEXT := TG_ARGV[1]; + tg_parent_id_column TEXT := TG_ARGV[2]; + tg_resource_name TEXT := TG_ARGV[3]; + + tg_instance_id TEXT; + tg_parent_id TEXT; + + select_ids TEXT := format('SELECT ($1).%I, ($1).%I', tg_instance_id_column, tg_parent_id_column); +BEGIN + IF (TG_OP = 'INSERT') THEN + EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING NEW; + + INSERT INTO projections.resource_counts(instance_id, table_name, parent_type, parent_id, resource_name) + VALUES (tg_instance_id, tg_table_name, tg_parent_type, tg_parent_id, tg_resource_name) + ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO + UPDATE SET updated_at = now(), amount = projections.resource_counts.amount + 1; + + RETURN NEW; + ELSEIF (TG_OP = 'DELETE') THEN + EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING OLD; + + UPDATE projections.resource_counts + SET updated_at = now(), amount = amount - 1 + WHERE instance_id = tg_instance_id + AND table_name = tg_table_name + AND parent_type = tg_parent_type + AND parent_id = tg_parent_id + AND resource_name = tg_resource_name + AND amount > 0; -- prevent check failure on negative amount. + + RETURN OLD; + END IF; +END +$$; + +-- delete_table_counts removes all resource counts for a TRUNCATED table. +CREATE OR REPLACE FUNCTION projections.delete_table_counts() + RETURNS trigger + LANGUAGE 'plpgsql' +AS $$ +DECLARE + -- trigger variables + tg_table_name TEXT := TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME; +BEGIN + DELETE FROM projections.resource_counts + WHERE table_name = tg_table_name; +END +$$; + +-- delete_parent_counts removes all resource counts for a deleted parent. +-- 1. The type of the parent +-- 2. The column name of the instance id +-- 3. The column name of the owner id +CREATE OR REPLACE FUNCTION projections.delete_parent_counts() + RETURNS trigger + LANGUAGE 'plpgsql' +AS $$ +DECLARE + -- trigger variables + tg_parent_type TEXT := TG_ARGV[0]; + tg_instance_id_column TEXT := TG_ARGV[1]; + tg_parent_id_column TEXT := TG_ARGV[2]; + + tg_instance_id TEXT; + tg_parent_id TEXT; + + select_ids TEXT := format('SELECT ($1).%I, ($1).%I', tg_instance_id_column, tg_parent_id_column); +BEGIN + EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING OLD; + + DELETE FROM projections.resource_counts + WHERE instance_id = tg_instance_id + AND parent_type = tg_parent_type + AND parent_id = tg_parent_id; + + RETURN OLD; +END +$$; diff --git a/cmd/setup/58.go b/cmd/setup/58.go new file mode 100644 index 0000000000..c46b30f548 --- /dev/null +++ b/cmd/setup/58.go @@ -0,0 +1,49 @@ +package setup + +import ( + "context" + "database/sql" + "embed" + "fmt" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 58/*.sql + replaceLoginNames3View embed.FS +) + +type ReplaceLoginNames3View struct { + dbClient *database.DB +} + +func (mig *ReplaceLoginNames3View) Execute(ctx context.Context, _ eventstore.Event) error { + var exists bool + err := mig.dbClient.QueryRowContext(ctx, func(r *sql.Row) error { + return r.Scan(&exists) + }, "SELECT exists(SELECT 1 from information_schema.views WHERE table_schema = 'projections' AND table_name = 'login_names3')") + + if err != nil || !exists { + return err + } + + statements, err := readStatements(replaceLoginNames3View, "58") + 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 (mig *ReplaceLoginNames3View) String() string { + return "58_replace_login_names3_view" +} diff --git a/cmd/setup/58/01_update_login_names3_view.sql b/cmd/setup/58/01_update_login_names3_view.sql new file mode 100644 index 0000000000..4499296152 --- /dev/null +++ b/cmd/setup/58/01_update_login_names3_view.sql @@ -0,0 +1,36 @@ +CREATE OR REPLACE VIEW projections.login_names3 AS + SELECT + u.id AS user_id + , CASE + WHEN p.must_be_domain THEN CONCAT(u.user_name, '@', d.name) + ELSE u.user_name + END AS login_name + , COALESCE(d.is_primary, TRUE) AS is_primary + , u.instance_id + FROM + projections.login_names3_users AS u + LEFT JOIN LATERAL ( + SELECT + must_be_domain + , is_default + FROM + projections.login_names3_policies AS p + WHERE + ( + p.instance_id = u.instance_id + AND NOT p.is_default + AND p.resource_owner = u.resource_owner + ) OR ( + p.instance_id = u.instance_id + AND p.is_default + ) + ORDER BY + p.is_default -- custom first + LIMIT 1 + ) AS p ON TRUE + LEFT JOIN + projections.login_names3_domains d + ON + p.must_be_domain + AND u.resource_owner = d.resource_owner + AND u.instance_id = d.instance_id diff --git a/cmd/setup/58/02_create_index.sql b/cmd/setup/58/02_create_index.sql new file mode 100644 index 0000000000..ed3627b427 --- /dev/null +++ b/cmd/setup/58/02_create_index.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS login_names3_policies_is_default_owner_idx ON projections.login_names3_policies (instance_id, is_default, resource_owner) INCLUDE (must_be_domain) diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 4742b94c7b..0c3f726902 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -150,6 +150,11 @@ type Steps struct { s51IDPTemplate6RootCA *IDPTemplate6RootCA s52IDPTemplate6LDAP2 *IDPTemplate6LDAP2 s53InitPermittedOrgsFunction *InitPermittedOrgsFunction53 + s54InstancePositionIndex *InstancePositionIndex + s55ExecutionHandlerStart *ExecutionHandlerStart + s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout + s57CreateResourceCounts *CreateResourceCounts + s58ReplaceLoginNames3View *ReplaceLoginNames3View } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 6043c38be4..18dd61241f 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -198,7 +198,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s35AddPositionToIndexEsWm = &AddPositionToIndexEsWm{dbClient: dbClient} steps.s36FillV2Milestones = &FillV3Milestones{dbClient: dbClient, eventstore: eventstoreClient} steps.s37Apps7OIDConfigsBackChannelLogoutURI = &Apps7OIDConfigsBackChannelLogoutURI{dbClient: dbClient} - steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: dbClient, esClient: eventstoreClient} + steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: dbClient} steps.s40InitPushFunc = &InitPushFunc{dbClient: dbClient} steps.s42Apps7OIDCConfigsLoginVersion = &Apps7OIDCConfigsLoginVersion{dbClient: dbClient} steps.s43CreateFieldsDomainIndex = &CreateFieldsDomainIndex{dbClient: dbClient} @@ -212,6 +212,11 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s51IDPTemplate6RootCA = &IDPTemplate6RootCA{dbClient: dbClient} steps.s52IDPTemplate6LDAP2 = &IDPTemplate6LDAP2{dbClient: dbClient} steps.s53InitPermittedOrgsFunction = &InitPermittedOrgsFunction53{dbClient: dbClient} + steps.s54InstancePositionIndex = &InstancePositionIndex{dbClient: dbClient} + steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient} + steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient} + steps.s57CreateResourceCounts = &CreateResourceCounts{dbClient: dbClient} + steps.s58ReplaceLoginNames3View = &ReplaceLoginNames3View{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -254,6 +259,11 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s51IDPTemplate6RootCA, steps.s52IDPTemplate6LDAP2, steps.s53InitPermittedOrgsFunction, + steps.s54InstancePositionIndex, + steps.s55ExecutionHandlerStart, + steps.s56IDPTemplate6SAMLFederatedLogout, + steps.s57CreateResourceCounts, + steps.s58ReplaceLoginNames3View, } { setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") if setupErr != nil { @@ -293,6 +303,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) dbClient: dbClient, }, } + repeatableSteps = append(repeatableSteps, triggerSteps(dbClient)...) for _, repeatableStep := range repeatableSteps { setupErr = executeMigration(ctx, eventstoreClient, repeatableStep, "unable to migrate repeatable step") diff --git a/cmd/setup/trigger_steps.go b/cmd/setup/trigger_steps.go new file mode 100644 index 0000000000..163a8fdb59 --- /dev/null +++ b/cmd/setup/trigger_steps.go @@ -0,0 +1,125 @@ +package setup + +import ( + "fmt" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/migration" + "github.com/zitadel/zitadel/internal/query/projection" +) + +// triggerSteps defines the repeatable migrations that set up triggers +// for counting resources in the database. +func triggerSteps(db *database.DB) []migration.RepeatableMigration { + return []migration.RepeatableMigration{ + // Delete parent count triggers for instances and organizations + migration.DeleteParentCountsTrigger(db, + projection.InstanceProjectionTable, + domain.CountParentTypeInstance, + projection.InstanceColumnID, + projection.InstanceColumnID, + "instance", + ), + migration.DeleteParentCountsTrigger(db, + projection.OrgProjectionTable, + domain.CountParentTypeOrganization, + projection.OrgColumnInstanceID, + projection.OrgColumnID, + "organization", + ), + + // Count triggers for all the resources + migration.CountTrigger(db, + projection.OrgProjectionTable, + domain.CountParentTypeInstance, + projection.OrgColumnInstanceID, + projection.OrgColumnInstanceID, + "organization", + ), + migration.CountTrigger(db, + projection.ProjectProjectionTable, + domain.CountParentTypeOrganization, + projection.ProjectColumnInstanceID, + projection.ProjectColumnResourceOwner, + "project", + ), + migration.CountTrigger(db, + projection.UserTable, + domain.CountParentTypeOrganization, + projection.UserInstanceIDCol, + projection.UserResourceOwnerCol, + "user", + ), + migration.CountTrigger(db, + projection.InstanceMemberProjectionTable, + domain.CountParentTypeInstance, + projection.MemberInstanceID, + projection.MemberResourceOwner, + "iam_admin", + ), + migration.CountTrigger(db, + projection.IDPTable, + domain.CountParentTypeInstance, + projection.IDPInstanceIDCol, + projection.IDPInstanceIDCol, + "identity_provider", + ), + migration.CountTrigger(db, + projection.IDPTemplateLDAPTable, + domain.CountParentTypeInstance, + projection.LDAPInstanceIDCol, + projection.LDAPInstanceIDCol, + "identity_provider_ldap", + ), + migration.CountTrigger(db, + projection.ActionTable, + domain.CountParentTypeInstance, + projection.ActionInstanceIDCol, + projection.ActionInstanceIDCol, + "action_v1", + ), + migration.CountTrigger(db, + projection.ExecutionTable, + domain.CountParentTypeInstance, + projection.ExecutionInstanceIDCol, + projection.ExecutionInstanceIDCol, + "execution", + ), + migration.CountTrigger(db, + fmt.Sprintf("%s_%s", projection.ExecutionTable, projection.ExecutionTargetSuffix), + domain.CountParentTypeInstance, + projection.ExecutionTargetInstanceIDCol, + projection.ExecutionTargetInstanceIDCol, + "execution_target", + ), + migration.CountTrigger(db, + projection.LoginPolicyTable, + domain.CountParentTypeInstance, + projection.LoginPolicyInstanceIDCol, + projection.LoginPolicyInstanceIDCol, + "login_policy", + ), + migration.CountTrigger(db, + projection.PasswordComplexityTable, + domain.CountParentTypeInstance, + projection.ComplexityPolicyInstanceIDCol, + projection.ComplexityPolicyInstanceIDCol, + "password_complexity_policy", + ), + migration.CountTrigger(db, + projection.PasswordAgeTable, + domain.CountParentTypeInstance, + projection.AgePolicyInstanceIDCol, + projection.AgePolicyInstanceIDCol, + "password_expiry_policy", + ), + migration.CountTrigger(db, + projection.LockoutPolicyTable, + domain.CountParentTypeInstance, + projection.LockoutPolicyInstanceIDCol, + projection.LockoutPolicyInstanceIDCol, + "lockout_policy", + ), + } +} diff --git a/cmd/start/start.go b/cmd/start/start.go index e3d84625b4..8820480f0c 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -40,11 +40,13 @@ import ( feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2" feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta" idp_v2 "github.com/zitadel/zitadel/internal/api/grpc/idp/v2" + instance "github.com/zitadel/zitadel/internal/api/grpc/instance/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/management" oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2" 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" + project_v2beta "github.com/zitadel/zitadel/internal/api/grpc/project/v2beta" "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" @@ -72,12 +74,14 @@ import ( "github.com/zitadel/zitadel/internal/authz" authz_repo "github.com/zitadel/zitadel/internal/authz/repository" authz_es "github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/eventstore" + "github.com/zitadel/zitadel/internal/cache" "github.com/zitadel/zitadel/internal/cache/connector" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" cryptoDB "github.com/zitadel/zitadel/internal/crypto/database" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/domain/federatedlogout" "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" @@ -304,7 +308,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server execution.Register( ctx, - config.Projections.Customizations["executions"], + config.Projections.Customizations["execution_handler"], config.Executions, queries, eventstoreClient.EventTypes(), @@ -442,6 +446,9 @@ func startAPIs( if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain), tlsConfig); err != nil { return nil, err } + if err := apis.RegisterService(ctx, instance.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain)); err != nil { + return nil, err + } if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.DatabaseName(), commands, queries, keys.User, config.AuditLogRetention), tlsConfig); err != nil { return nil, err } @@ -454,7 +461,7 @@ func startAPIs( if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, user_v2.CreateServer(config.SystemDefaults, commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { @@ -463,7 +470,7 @@ func startAPIs( if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, org_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, org_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, feature_v2beta.CreateServer(commands, queries)); err != nil { @@ -487,6 +494,9 @@ func startAPIs( 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, project_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { + return nil, err + } if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil { return nil, err } @@ -503,7 +513,12 @@ func startAPIs( assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) 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)) + federatedLogoutsCache, err := connector.StartCache[federatedlogout.Index, string, *federatedlogout.FederatedLogout](ctx, []federatedlogout.Index{federatedlogout.IndexRequestID}, cache.PurposeFederatedLogout, cacheConnectors.Config.FederatedLogouts, cacheConnectors) + if err != nil { + return nil, err + } + + apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler, federatedLogoutsCache)) userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost, login.EndpointSAMLACS) if err != nil { @@ -524,7 +539,25 @@ func startAPIs( } apis.RegisterHandlerOnPrefix(openapi.HandlerPrefix, openAPIHandler) - oidcServer, err := oidc.NewServer(ctx, config.OIDC, login.DefaultLoggedOutPath, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.OIDCKey, eventstore, dbClient, userAgentInterceptor, instanceInterceptor.Handler, limitingAccessInterceptor, config.Log.Slog(), config.SystemDefaults.SecretHasher) + oidcServer, err := oidc.NewServer( + ctx, + config.OIDC, + login.DefaultLoggedOutPath, + config.ExternalSecure, + commands, + queries, + authRepo, + keys.OIDC, + keys.OIDCKey, + eventstore, + dbClient, + userAgentInterceptor, + instanceInterceptor.Handler, + limitingAccessInterceptor, + config.Log.Slog(), + config.SystemDefaults.SecretHasher, + federatedLogoutsCache, + ) if err != nil { return nil, fmt.Errorf("unable to start oidc provider: %w", err) } @@ -573,6 +606,7 @@ func startAPIs( keys.IDPConfig, keys.CSRFCookieKey, cacheConnectors, + federatedLogoutsCache, ) if err != nil { return nil, fmt.Errorf("unable to start login: %w", err) diff --git a/cmd/start/start_from_init.go b/cmd/start/start_from_init.go index 62d705b33c..41972e16ad 100644 --- a/cmd/start/start_from_init.go +++ b/cmd/start/start_from_init.go @@ -1,6 +1,8 @@ package start import ( + "context" + "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" @@ -29,14 +31,19 @@ Requirements: masterKey, err := key.MasterKey(cmd) logging.OnError(err).Panic("No master key provided") - initialise.InitAll(cmd.Context(), initialise.MustNewConfig(viper.GetViper())) + initCtx, cancel := context.WithCancel(cmd.Context()) + initialise.InitAll(initCtx, initialise.MustNewConfig(viper.GetViper())) + cancel() err = setup.BindInitProjections(cmd) logging.OnError(err).Fatal("unable to bind \"init-projections\" flag") setupConfig := setup.MustNewConfig(viper.GetViper()) setupSteps := setup.MustNewSteps(viper.New()) - setup.Setup(cmd.Context(), setupConfig, setupSteps, masterKey) + + setupCtx, cancel := context.WithCancel(cmd.Context()) + setup.Setup(setupCtx, setupConfig, setupSteps, masterKey) + cancel() startConfig := MustNewConfig(viper.GetViper()) diff --git a/cmd/start/start_from_setup.go b/cmd/start/start_from_setup.go index a8b7295f2a..3e8a13705e 100644 --- a/cmd/start/start_from_setup.go +++ b/cmd/start/start_from_setup.go @@ -1,6 +1,8 @@ package start import ( + "context" + "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" @@ -34,7 +36,10 @@ Requirements: setupConfig := setup.MustNewConfig(viper.GetViper()) setupSteps := setup.MustNewSteps(viper.New()) - setup.Setup(cmd.Context(), setupConfig, setupSteps, masterKey) + + setupCtx, cancel := context.WithCancel(cmd.Context()) + setup.Setup(setupCtx, setupConfig, setupSteps, masterKey) + cancel() startConfig := MustNewConfig(viper.GetViper()) diff --git a/console/package.json b/console/package.json index 2d986730c2..0095360017 100644 --- a/console/package.json +++ b/console/package.json @@ -31,8 +31,8 @@ "@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", + "@zitadel/client": "1.2.0", + "@zitadel/proto": "1.2.0", "angular-oauth2-oidc": "^15.0.1", "angularx-qrcode": "^16.0.2", "buffer": "^6.0.3", @@ -82,6 +82,7 @@ "jasmine-spec-reporter": "~7.0.0", "karma": "^6.4.4", "karma-chrome-launcher": "^3.2.0", + "karma-coverage": "^2.2.1", "karma-coverage-istanbul-reporter": "^3.0.3", "karma-jasmine": "^5.1.0", "karma-jasmine-html-reporter": "^2.1.0", diff --git a/console/src/app/components/oidc-configuration/oidc-configuration.component.spec.ts b/console/src/app/components/oidc-configuration/oidc-configuration.component.spec.ts index 6155ba5693..e99aee357c 100644 --- a/console/src/app/components/oidc-configuration/oidc-configuration.component.spec.ts +++ b/console/src/app/components/oidc-configuration/oidc-configuration.component.spec.ts @@ -1,16 +1,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { QuickstartComponent } from './quickstart.component'; +import { OIDCConfigurationComponent } from './oidc-configuration.component'; describe('QuickstartComponent', () => { - let component: QuickstartComponent; - let fixture: ComponentFixture; + let component: OIDCConfigurationComponent; + let fixture: ComponentFixture; beforeEach(() => { TestBed.configureTestingModule({ - declarations: [QuickstartComponent], + declarations: [OIDCConfigurationComponent], }); - fixture = TestBed.createComponent(QuickstartComponent); + fixture = TestBed.createComponent(OIDCConfigurationComponent); component = fixture.componentInstance; fixture.detectChanges(); }); 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 index 82f04fb124..7948ba7554 100644 --- 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 @@ -24,8 +24,8 @@ {{ 'ACTIONSTWO.EXECUTION.TABLE.TARGET' | translate }}
- {{ target.name }} + + {{ target.name }}
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 index 2d9942c406..af9673dbf5 100644 --- 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 @@ -55,13 +55,9 @@ export class ActionsTwoActionsTableComponent { } 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; - }); + const mappedTargets = execution.targets + .map((target) => targetsMap.get(target)) + .filter((target): target is NonNullable => !!target); return { execution, mappedTargets }; }); }); 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 index c22b03ef76..3e6c31fc0e 100644 --- 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 @@ -1,4 +1,7 @@

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

+ + {{ 'ACTIONSTWO.BETA_NOTE' | translate }} +

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

{ - return this.actionService.listExecutions({}); + return this.actionService.listExecutions({ sortingColumn: ExecutionFieldName.ID, pagination: { asc: true } }); }), map(({ result }) => result.map(correctlyTypeExecution)), catchError((err) => { @@ -110,4 +112,6 @@ export class ActionsTwoActionsComponent { this.toast.showError(error); } } + + protected readonly InfoSectionType = InfoSectionType; } 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 index 401e5e521d..f0248f45a2 100644 --- 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 @@ -84,7 +84,7 @@
{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.TITLE' | translate }} {{ - 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.DESCRIPTION' | translate + 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL_EVENTS' | translate }}
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 index 12ae6598cc..a93d03e914 100644 --- 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 @@ -10,12 +10,7 @@ import { } 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 { Condition, Execution } from '@zitadel/proto/zitadel/action/v2beta/execution_pb'; import { Subject } from 'rxjs'; import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; @@ -27,11 +22,8 @@ enum Page { export type CorrectlyTypedCondition = Condition & { conditionType: Extract }; -type CorrectlyTypedTargets = { type: Extract }; - -export type CorrectlyTypedExecution = Omit & { +export type CorrectlyTypedExecution = Omit & { condition: CorrectlyTypedCondition; - targets: CorrectlyTypedTargets[]; }; export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExecution => { @@ -48,9 +40,6 @@ export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExec return { ...execution, condition, - targets: execution.targets - .map(({ type }) => ({ type })) - .filter((target): target is CorrectlyTypedTargets => target.type.case === 'target'), }; }; @@ -81,7 +70,7 @@ export class ActionTwoAddActionDialogComponent { protected readonly typeSignal = signal('request'); protected readonly conditionSignal = signal['condition']>(undefined); - protected readonly targetsSignal = signal[]>([]); + protected readonly targetsSignal = signal([]); protected readonly continueSubject = new Subject(); @@ -112,7 +101,7 @@ export class ActionTwoAddActionDialogComponent { 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.preselectedTargetIds = data.execution.targets; this.page.set(Page.Target); // Set the initial page based on the provided execution data } 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 index 422ed7991e..26d9e3be2c 100644 --- 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 @@ -1,4 +1,4 @@ -
+

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

@@ -8,9 +8,9 @@ #trigger="matAutocompleteTrigger" #input type="text" - [formControl]="form().controls.autocomplete" + [formControl]="form.controls.autocomplete" [matAutocomplete]="autoservice" - (keydown.enter)="handleEnter($event); input.blur(); trigger.closePanel()" + (keydown.enter)="handleEnter($event, form); input.blur(); trigger.closePanel()" /> @@ -19,7 +19,7 @@ {{ target.name }} @@ -27,7 +27,7 @@ - +
+ + + + + + + +
Reorder @@ -48,7 +48,7 @@ actions matTooltip="{{ 'ACTIONS.REMOVE' | translate }}" color="warn" - (click)="removeTarget(i)" + (click)="removeTarget(i, form)" mat-icon-button > @@ -65,7 +65,7 @@ {{ 'ACTIONS.BACK' | translate }} - 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 index e04368f8f4..60b4025650 100644 --- 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 @@ -14,7 +14,7 @@ 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 { ObservedValueOf, ReplaySubject, shareReplay, 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'; @@ -23,14 +23,13 @@ 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 { map, 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 { 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'; @@ -72,11 +71,12 @@ export class ActionsTwoAddActionTargetComponent { } @Output() public readonly back = new EventEmitter(); - @Output() public readonly continue = new EventEmitter[]>(); + @Output() public readonly continue = new EventEmitter(); private readonly preselectedTargetIds$ = new ReplaySubject(1); - protected readonly form: ReturnType; + protected readonly form$: ReturnType; + protected readonly targets: ReturnType; private readonly selectedTargetIds: Signal; protected readonly selectableTargets: Signal; @@ -87,26 +87,27 @@ export class ActionsTwoAddActionTargetComponent { private readonly actionService: ActionService, private readonly toast: ToastService, ) { - this.form = this.buildForm(); + this.form$ = this.buildForm().pipe(shareReplay({ refCount: true, bufferSize: 1 })); this.targets = this.listTargets(); - this.selectedTargetIds = this.getSelectedTargetIds(this.form); - this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds); + this.selectedTargetIds = this.getSelectedTargetIds(this.form$); + this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds, this.form$); 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)], - }), - }); - }); + return this.preselectedTargetIds$.pipe( + startWith([] as string[]), + map((preselectedTargetIds) => { + return this.fb.group({ + autocomplete: new FormControl('', { nonNullable: true }), + selectedTargetIds: new FormControl(preselectedTargetIds, { + nonNullable: true, + validators: [minArrayLengthValidator(1)], + }), + }); + }), + ); } private listTargets() { @@ -129,25 +130,35 @@ export class ActionsTwoAddActionTargetComponent { return computed(targetsSignal); } - private getSelectedTargetIds(form: typeof this.form) { - const selectedTargetIds$ = toObservable(form).pipe( - startWith(form()), - switchMap((form) => { - const { selectedTargetIds } = form.controls; + private getSelectedTargetIds(form$: typeof this.form$) { + const selectedTargetIds$ = form$.pipe( + switchMap(({ controls: { selectedTargetIds } }) => { return selectedTargetIds.valueChanges.pipe(startWith(selectedTargetIds.value)); }), ); return toSignal(selectedTargetIds$, { requireSync: true }); } - private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal) { - return computed(() => { + private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal, form$: typeof this.form$) { + const autocomplete$ = form$.pipe( + switchMap(({ controls: { autocomplete } }) => { + return autocomplete.valueChanges.pipe(startWith(autocomplete.value)); + }), + ); + const autocompleteSignal = toSignal(autocomplete$, { requireSync: true }); + + const unselectedTargets = computed(() => { const targetsCopy = new Map(targets().targets); for (const selectedTargetId of selectedTargetIds()) { targetsCopy.delete(selectedTargetId); } return Array.from(targetsCopy.values()); }); + + return computed(() => { + const autocomplete = autocompleteSignal().toLowerCase(); + return unselectedTargets().filter(({ name }) => name.toLowerCase().includes(autocomplete)); + }); } private getDataSource(targetsSignal: typeof this.targets, selectedTargetIdsSignal: Signal) { @@ -178,46 +189,39 @@ export class ActionsTwoAddActionTargetComponent { return dataSource; } - protected addTarget(target: Target) { - const { selectedTargetIds } = this.form().controls; + protected addTarget(target: Target, form: ObservedValueOf) { + const { selectedTargetIds } = form.controls; selectedTargetIds.setValue([target.id, ...selectedTargetIds.value]); - this.form().controls.autocomplete.setValue(''); + form.controls.autocomplete.setValue(''); } - protected removeTarget(index: number) { - const { selectedTargetIds } = this.form().controls; + protected removeTarget(index: number, form: ObservedValueOf) { + const { selectedTargetIds } = form.controls; const data = [...selectedTargetIds.value]; data.splice(index, 1); selectedTargetIds.setValue(data); } - protected drop(event: CdkDragDrop) { - const { selectedTargetIds } = this.form().controls; + protected drop(event: CdkDragDrop, form: ObservedValueOf) { + const { selectedTargetIds } = form.controls; const data = [...selectedTargetIds.value]; moveItemInArray(data, event.previousIndex, event.currentIndex); selectedTargetIds.setValue(data); } - protected handleEnter(event: Event) { + protected handleEnter(event: Event, form: ObservedValueOf) { const selectableTargets = this.selectableTargets(); if (selectableTargets.length !== 1) { return; } event.preventDefault(); - this.addTarget(selectableTargets[0]); + this.addTarget(selectableTargets[0], form); } protected submit() { - const selectedTargets = this.selectedTargetIds().map((value) => ({ - type: { - case: 'target' as const, - value, - }, - })); - - this.continue.emit(selectedTargets); + this.continue.emit(this.selectedTargetIds()); } protected trackTarget(_: number, target: Target) { 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 index 37d4f89dd0..717ee8f850 100644 --- 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 @@ -26,6 +26,9 @@ {{ 'ACTIONSTWO.TARGET.CREATE.TYPES.' + type | translate }} + + {{ 'ACTIONSTWO.TARGET.CREATE.TYPES_DESCRIPTION' | 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 index 34a7d5203d..a68e1e87cb 100644 --- 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 @@ -23,3 +23,7 @@ .name-hint { font-size: 12px; } + +.types-description { + white-space: pre-line; +} 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 index a6bde66e41..23b3b4bb89 100644 --- 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 @@ -1,4 +1,7 @@

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

+ + {{ 'ACTIONSTWO.BETA_NOTE' | translate }} +

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

setTimeout(res, 1000)); @@ -86,4 +88,6 @@ export class ActionsTwoTargetsComponent { this.toast.showError(error); } } + + protected readonly InfoSectionType = InfoSectionType; } diff --git a/console/src/app/modules/actions-two/actions-two.module.ts b/console/src/app/modules/actions-two/actions-two.module.ts index 45d70193f9..a940264eb2 100644 --- a/console/src/app/modules/actions-two/actions-two.module.ts +++ b/console/src/app/modules/actions-two/actions-two.module.ts @@ -20,6 +20,7 @@ import { ProjectRoleChipModule } from '../project-role-chip/project-role-chip.mo 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'; +import { InfoSectionModule } from '../info-section/info-section.module'; @NgModule({ declarations: [ @@ -47,6 +48,7 @@ import { MatIconModule } from '@angular/material/icon'; TypeSafeCellDefModule, ProjectRoleChipModule, ActionConditionPipeModule, + InfoSectionModule, ], exports: [ActionsTwoActionsComponent, ActionsTwoTargetsComponent, ActionsTwoTargetsTableComponent], }) diff --git a/console/src/app/modules/domains/domains.component.spec.ts b/console/src/app/modules/domains/domains.component.spec.ts index 127bae48b5..f3d75fb12b 100644 --- a/console/src/app/modules/domains/domains.component.spec.ts +++ b/console/src/app/modules/domains/domains.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { OrgDomainsComponent } from './org-domains.component'; +import { DomainsComponent } from './domains.component'; describe('OrgDomainsComponent', () => { - let component: OrgDomainsComponent; - let fixture: ComponentFixture; + let component: DomainsComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [OrgDomainsComponent], + declarations: [DomainsComponent], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(OrgDomainsComponent); + fixture = TestBed.createComponent(DomainsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/filter-org/filter-org.component.html b/console/src/app/modules/filter-org/filter-org.component.html index 4e8535fe76..ae42667f49 100644 --- a/console/src/app/modules/filter-org/filter-org.component.html +++ b/console/src/app/modules/filter-org/filter-org.component.html @@ -69,4 +69,19 @@
+ +
+ {{ 'FILTER.ORGID' | translate }} + +
+ + {{ 'FILTER.METHODS.1' | 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 220b219358..4ea9a6ea6e 100644 --- a/console/src/app/modules/filter-org/filter-org.component.ts +++ b/console/src/app/modules/filter-org/filter-org.component.ts @@ -3,7 +3,14 @@ import { MatCheckboxChange } from '@angular/material/checkbox'; import { ActivatedRoute, Router } from '@angular/router'; import { take } from 'rxjs'; import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb'; -import { OrgDomainQuery, OrgNameQuery, OrgQuery, OrgState, OrgStateQuery } from 'src/app/proto/generated/zitadel/org_pb'; +import { + OrgDomainQuery, + OrgNameQuery, + OrgQuery, + OrgState, + OrgStateQuery, + OrgIDQuery, +} from 'src/app/proto/generated/zitadel/org_pb'; import { UserNameQuery } from 'src/app/proto/generated/zitadel/user_pb'; import { FilterComponent } from '../filter/filter.component'; @@ -12,6 +19,7 @@ enum SubQuery { NAME, STATE, DOMAIN, + ID, } @Component({ @@ -61,6 +69,12 @@ export class FilterOrgComponent extends FilterComponent implements OnInit { orgDomainQuery.setMethod(filter.domainQuery.method); orgQuery.setDomainQuery(orgDomainQuery); return orgQuery; + } else if (filter.idQuery) { + const orgQuery = new OrgQuery(); + const orgIdQuery = new OrgIDQuery(); + orgIdQuery.setId(filter.idQuery.id); + orgQuery.setIdQuery(orgIdQuery); + return orgQuery; } else { return undefined; } @@ -100,6 +114,13 @@ export class FilterOrgComponent extends FilterComponent implements OnInit { odq.setDomainQuery(dq); this.searchQueries.push(odq); break; + case SubQuery.ID: + const idq = new OrgIDQuery(); + idq.setId(''); + const oidq = new OrgQuery(); + oidq.setIdQuery(idq); + this.searchQueries.push(oidq); + break; } } else { switch (subquery) { @@ -121,6 +142,12 @@ export class FilterOrgComponent extends FilterComponent implements OnInit { this.searchQueries.splice(index_pdn, 1); } break; + case SubQuery.ID: + const index_id = this.searchQueries.findIndex((q) => (q as OrgQuery).toObject().idQuery !== undefined); + if (index_id > -1) { + this.searchQueries.splice(index_id, 1); + } + break; } } } @@ -140,6 +167,10 @@ export class FilterOrgComponent extends FilterComponent implements OnInit { (query as OrgDomainQuery).setDomain(value); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []); break; + case SubQuery.ID: + (query as OrgIDQuery).setId(value); + this.filterChanged.emit(this.searchQueries ? this.searchQueries : []); + break; } } @@ -166,6 +197,13 @@ export class FilterOrgComponent extends FilterComponent implements OnInit { } else { return undefined; } + case SubQuery.ID: + const id = this.searchQueries.find((q) => (q as OrgQuery).toObject().idQuery !== undefined); + if (id) { + return (id as OrgQuery).getIdQuery(); + } else { + return undefined; + } } } diff --git a/console/src/app/modules/filter-project/filter-project.component.spec.ts b/console/src/app/modules/filter-project/filter-project.component.spec.ts index 0ed0436db8..ff465d8705 100644 --- a/console/src/app/modules/filter-project/filter-project.component.spec.ts +++ b/console/src/app/modules/filter-project/filter-project.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FilterUserComponent } from './filter-user.component'; +import { FilterProjectComponent } from './filter-project.component'; describe('FilterUserComponent', () => { - let component: FilterUserComponent; - let fixture: ComponentFixture; + let component: FilterProjectComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [FilterUserComponent], + declarations: [FilterProjectComponent], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(FilterUserComponent); + fixture = TestBed.createComponent(FilterProjectComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/info-row/info-row.component.html b/console/src/app/modules/info-row/info-row.component.html index 52bd6b93ba..1acd013fbe 100644 --- a/console/src/app/modules/info-row/info-row.component.html +++ b/console/src/app/modules/info-row/info-row.component.html @@ -2,7 +2,7 @@

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

{{ 'IAM.PAGES.STATE' | translate }}

{{ 'RESOURCEID' | translate }}

-

{{ instance.id }}

+

{{ instance.id }}

{{ 'NAME' | translate }}

-

{{ instance.name }}

+

{{ instance.name }}

{{ 'VERSION' | translate }}

-

{{ instance.version }}

+

{{ instance.version }}

@@ -96,15 +96,15 @@

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

-

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

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

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

-

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

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

@@ -113,7 +113,7 @@

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

@@ -123,7 +123,7 @@

{{ 'RESOURCEID' | translate }}

-

{{ org.id }}

+

{{ org.id }}

@@ -143,15 +143,15 @@

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

-

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

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

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

-

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

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

@@ -160,7 +160,7 @@

{{ 'PROJECT.STATE.TITLE' | translate }}

{{ 'RESOURCEID' | translate }}

-

{{ project.id }}

+

{{ project.id }}

{{ 'PROJECT.PAGES.CREATEDON' | translate }}

-

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

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

{{ 'PROJECT.PAGES.LASTMODIFIED' | translate }}

-

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

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

@@ -195,7 +195,7 @@

{{ 'PROJECT.STATE.TITLE' | translate }}

{{ 'RESOURCEID' | translate }}

-

{{ grantedProject.projectId }}

+

{{ grantedProject.projectId }}

{{ 'PROJECT.GRANT.GRANTID' | translate }}

-

{{ grantedProject.grantId }}

+

{{ grantedProject.grantId }}

{{ 'PROJECT.PAGES.CREATEDON' | translate }}

-

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

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

{{ 'PROJECT.PAGES.LASTMODIFIED' | translate }}

-

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

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

@@ -236,30 +236,43 @@

{{ 'APP.PAGES.STATE' | translate }}

{{ 'APP.PAGES.DETAIL.STATE.' + app.state | translate }}

+
+

{{ 'APP.AUTHMETHOD' | translate }}

+

+ {{ 'APP.API.AUTHMETHOD.' + authMethodType | translate }} +

+
+ +
+

{{ 'APP.AUTHMETHOD' | translate }}

+

+ {{ 'APP.OIDC.AUTHMETHOD.' + authMethodType | translate }} +

+

{{ 'APP.PAGES.ID' | translate }}

-

{{ app.id }}

+

{{ app.id }}

{{ 'APP.PAGES.DATECREATED' | translate }}

-

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

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

{{ 'APP.PAGES.DATECHANGED' | translate }}

-

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

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

@@ -267,27 +280,27 @@

{{ 'APP.OIDC.INFO.CLIENTID' | translate }}

@@ -304,22 +317,22 @@

{{ 'IDP.DETAIL.DATECREATED' | translate }}

-

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

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

{{ 'IDP.DETAIL.DATECHANGED' | translate }}

-

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

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

{{ 'IDP.STATE' | translate }}

diff --git a/console/src/app/modules/input/input.directive.spec.ts b/console/src/app/modules/input/input.directive.spec.ts index 463fed5431..46544ca096 100644 --- a/console/src/app/modules/input/input.directive.spec.ts +++ b/console/src/app/modules/input/input.directive.spec.ts @@ -1,8 +1,49 @@ +import { Component, ElementRef, NgZone } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; import { InputDirective } from './input.directive'; +import { Platform } from '@angular/cdk/platform'; +import { NgControl, NgForm, FormGroupDirective } from '@angular/forms'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { AutofillMonitor } from '@angular/cdk/text-field'; +import { MatFormField } from '@angular/material/form-field'; +import { MAT_INPUT_VALUE_ACCESSOR } from '@angular/material/input'; +import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; + +@Component({ + template: ``, +}) +class TestHostComponent {} describe('InputDirective', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [InputDirective, TestHostComponent], + providers: [ + { provide: ElementRef, useValue: new ElementRef(document.createElement('input')) }, + Platform, + { provide: NgControl, useValue: null }, + { provide: NgForm, useValue: null }, + { provide: FormGroupDirective, useValue: null }, + ErrorStateMatcher, + { provide: MAT_INPUT_VALUE_ACCESSOR, useValue: null }, + { + provide: AutofillMonitor, + useValue: { monitor: () => of(), stopMonitoring: () => {} }, + }, + NgZone, + { provide: MatFormField, useValue: null }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + }); + it('should create an instance', () => { - const directive = new InputDirective(); - expect(directive).toBeTruthy(); + const directiveEl = fixture.debugElement.query(By.directive(InputDirective)); + expect(directiveEl).toBeTruthy(); }); }); diff --git a/console/src/app/modules/label/label.component.spec.ts b/console/src/app/modules/label/label.component.spec.ts index 2b29b30873..e719aa3775 100644 --- a/console/src/app/modules/label/label.component.spec.ts +++ b/console/src/app/modules/label/label.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AvatarComponent } from './avatar.component'; +import { LabelComponent } from './label.component'; describe('AvatarComponent', () => { - let component: AvatarComponent; - let fixture: ComponentFixture; + let component: LabelComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [AvatarComponent], + declarations: [LabelComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(AvatarComponent); + fixture = TestBed.createComponent(LabelComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts index 8deff09eee..c75e15bf04 100644 --- a/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts +++ b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts @@ -4,7 +4,6 @@ import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { ToastService } from 'src/app/services/toast.service'; import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; -import { Buffer } from 'buffer'; export type MetadataDialogData = { metadata: (Metadata.AsObject | MetadataV2)[]; @@ -26,9 +25,10 @@ export class MetadataDialogComponent { public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: MetadataDialogData, ) { + const decoder = new TextDecoder(); this.metadata = data.metadata.map(({ key, value }) => ({ key, - value: typeof value === 'string' ? value : Buffer.from(value as unknown as string, 'base64').toString('utf8'), + value: typeof value === 'string' ? value : decoder.decode(value), })); } diff --git a/console/src/app/modules/metadata/metadata/metadata.component.ts b/console/src/app/modules/metadata/metadata/metadata.component.ts index 7f72297c00..bdb2c7734c 100644 --- a/console/src/app/modules/metadata/metadata/metadata.component.ts +++ b/console/src/app/modules/metadata/metadata/metadata.component.ts @@ -5,7 +5,6 @@ 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; @@ -37,12 +36,13 @@ export class MetadataComponent implements OnInit { ngOnInit() { this.dataSource$ = this.metadata$.pipe( - map((metadata) => - metadata.map(({ key, value }) => ({ + map((metadata) => { + const decoder = new TextDecoder(); + return metadata.map(({ key, value }) => ({ key, - value: Buffer.from(value as any as string, 'base64').toString('utf-8'), - })), - ), + value: typeof value === 'string' ? value : decoder.decode(value), + })); + }), startWith([] as StringMetadata[]), map((metadata) => new MatTableDataSource(metadata)), ); diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.ts b/console/src/app/modules/policies/login-policy/login-policy.component.ts index b4e4557f00..5dd85b8b38 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.ts +++ b/console/src/app/modules/policies/login-policy/login-policy.component.ts @@ -2,14 +2,12 @@ import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/co import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Duration } from 'google-protobuf/google/protobuf/duration_pb'; -import { firstValueFrom, forkJoin, from, Observable, of, Subject, take } from 'rxjs'; +import { forkJoin, from, of, Subject, take } from 'rxjs'; import { GetLoginPolicyResponse as AdminGetLoginPolicyResponse, UpdateLoginPolicyRequest, - UpdateLoginPolicyResponse, } from 'src/app/proto/generated/zitadel/admin_pb'; import { - AddCustomLoginPolicyRequest, GetLoginPolicyResponse as MgmtGetLoginPolicyResponse, UpdateCustomLoginPolicyRequest, } from 'src/app/proto/generated/zitadel/management_pb'; @@ -24,8 +22,7 @@ import { InfoSectionType } from '../../info-section/info-section.component'; import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component'; import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { LoginMethodComponentType } from './factor-table/factor-table.component'; -import { catchError, map, takeUntil } from 'rxjs/operators'; -import { error } from 'console'; +import { map, takeUntil } from 'rxjs/operators'; import { LoginPolicyService } from '../../../services/login-policy.service'; const minValueValidator = (minValue: number) => (control: AbstractControl) => { diff --git a/console/src/app/modules/policies/message-texts/message-texts.component.spec.ts b/console/src/app/modules/policies/message-texts/message-texts.component.spec.ts index 71dd427da5..e5569f9ed3 100644 --- a/console/src/app/modules/policies/message-texts/message-texts.component.spec.ts +++ b/console/src/app/modules/policies/message-texts/message-texts.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { LoginPolicyComponent } from './login-policy.component'; +import { MessageTextsComponent } from './message-texts.component'; describe('LoginPolicyComponent', () => { - let component: LoginPolicyComponent; - let fixture: ComponentFixture; + let component: MessageTextsComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [LoginPolicyComponent], + declarations: [MessageTextsComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(LoginPolicyComponent); + fixture = TestBed.createComponent(MessageTextsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts b/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts index c323d884f1..f529e143a5 100644 --- a/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts +++ b/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component'; +import { NotificationPolicyComponent } from './notification-policy.component'; describe('PasswordComplexityPolicyComponent', () => { - let component: PasswordComplexityPolicyComponent; - let fixture: ComponentFixture; + let component: NotificationPolicyComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [PasswordComplexityPolicyComponent], + declarations: [NotificationPolicyComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(PasswordComplexityPolicyComponent); + fixture = TestBed.createComponent(NotificationPolicyComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/policies/notification-sms-provider/password-dialog-sms-provider/password-dialog-sms-provider.component.spec.ts b/console/src/app/modules/policies/notification-sms-provider/password-dialog-sms-provider/password-dialog-sms-provider.component.spec.ts index 034bbe8de0..b009b03757 100644 --- a/console/src/app/modules/policies/notification-sms-provider/password-dialog-sms-provider/password-dialog-sms-provider.component.spec.ts +++ b/console/src/app/modules/policies/notification-sms-provider/password-dialog-sms-provider/password-dialog-sms-provider.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { PasswordDialogComponent } from './password-dialog-sms-provider.component'; +import { PasswordDialogSMSProviderComponent } from './password-dialog-sms-provider.component'; describe('PasswordDialogComponent', () => { - let component: PasswordDialogComponent; - let fixture: ComponentFixture; + let component: PasswordDialogSMSProviderComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [PasswordDialogComponent], + declarations: [PasswordDialogSMSProviderComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(PasswordDialogComponent); + fixture = TestBed.createComponent(PasswordDialogSMSProviderComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/providers/provider-github-es/provider-github-es.component.spec.ts b/console/src/app/modules/providers/provider-github-es/provider-github-es.component.spec.ts index 0086bf0ce3..304004d0cf 100644 --- a/console/src/app/modules/providers/provider-github-es/provider-github-es.component.spec.ts +++ b/console/src/app/modules/providers/provider-github-es/provider-github-es.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ProviderOAuthComponent } from './provider-oauth.component'; +import { ProviderGithubESComponent } from './provider-github-es.component'; describe('ProviderOAuthComponent', () => { - let component: ProviderOAuthComponent; - let fixture: ComponentFixture; + let component: ProviderGithubESComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ProviderOAuthComponent], + declarations: [ProviderGithubESComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ProviderOAuthComponent); + fixture = TestBed.createComponent(ProviderGithubESComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.spec.ts b/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.spec.ts index 3b6fdadce3..5a0bbf6d08 100644 --- a/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.spec.ts +++ b/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ProviderGoogleComponent } from './provider-google.component'; +import { ProviderGitlabSelfHostedComponent } from './provider-gitlab-self-hosted.component'; describe('ProviderGoogleComponent', () => { - let component: ProviderGoogleComponent; - let fixture: ComponentFixture; + let component: ProviderGitlabSelfHostedComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ProviderGoogleComponent], + declarations: [ProviderGitlabSelfHostedComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ProviderGoogleComponent); + fixture = TestBed.createComponent(ProviderGitlabSelfHostedComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.spec.ts b/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.spec.ts index 3b6fdadce3..7b5becd782 100644 --- a/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.spec.ts +++ b/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ProviderGoogleComponent } from './provider-google.component'; +import { ProviderGitlabComponent } from './provider-gitlab.component'; describe('ProviderGoogleComponent', () => { - let component: ProviderGoogleComponent; - let fixture: ComponentFixture; + let component: ProviderGitlabComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ProviderGoogleComponent], + declarations: [ProviderGitlabComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ProviderGoogleComponent); + fixture = TestBed.createComponent(ProviderGitlabComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html b/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html index e326d3be26..c7bb48ea71 100644 --- a/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html +++ b/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html @@ -98,13 +98,12 @@

{{ 'IDP.ISIDTOKENMAPPING_DESC' | translate }}

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

{{ 'IDP.USEPKCE_DESC' | translate }}

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

{{ 'IDP.USEPKCE_DESC' | translate }}

+ {{ 'IDP.USEPKCE' | translate }} +
+
diff --git a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html index d738b3fec9..245a963fa8 100644 --- a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html +++ b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html @@ -82,7 +82,7 @@
-

{{ 'IDP.SAML.TRANSIENTMAPPINGATTRIBUTENAME_DESC' | translate }}

+

{{ 'IDP.SAML.TRANSIENTMAPPINGATTRIBUTENAME_DESC' | translate }}

@@ -90,6 +90,15 @@
+ + +
+

{{ 'IDP.FEDERATEDLOGOUTENABLED_DESC' | translate }}

+ {{ + 'IDP.FEDERATEDLOGOUTENABLED' | translate + }} +
+
{ - let component: ShowKeyDialogComponent; - let fixture: ComponentFixture; + let component: ShowTokenDialogComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ShowKeyDialogComponent], + declarations: [ShowTokenDialogComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ShowKeyDialogComponent); + fixture = TestBed.createComponent(ShowTokenDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/sidenav/sidenav.component.html b/console/src/app/modules/sidenav/sidenav.component.html index 277686852b..78e21e79f6 100644 --- a/console/src/app/modules/sidenav/sidenav.component.html +++ b/console/src/app/modules/sidenav/sidenav.component.html @@ -28,6 +28,7 @@ [attr.data-e2e]="'sidenav-element-' + setting.id" > {{ setting.i18nKey | translate }} + {{ 'SETTINGS.BETA' | translate }} diff --git a/console/src/app/modules/sidenav/sidenav.component.scss b/console/src/app/modules/sidenav/sidenav.component.scss index 383857751c..bb55a6999d 100644 --- a/console/src/app/modules/sidenav/sidenav.component.scss +++ b/console/src/app/modules/sidenav/sidenav.component.scss @@ -90,6 +90,10 @@ flex-shrink: 0; } + .state { + margin-left: 0.5rem; + } + &:hover { span { opacity: 1; diff --git a/console/src/app/modules/sidenav/sidenav.component.ts b/console/src/app/modules/sidenav/sidenav.component.ts index 33539750a2..4b73491f08 100644 --- a/console/src/app/modules/sidenav/sidenav.component.ts +++ b/console/src/app/modules/sidenav/sidenav.component.ts @@ -11,6 +11,7 @@ export interface SidenavSetting { [PolicyComponentServiceType.ADMIN]?: string[]; }; showWarn?: boolean; + beta?: boolean; } @Component({ diff --git a/console/src/app/modules/smtp-table/smtp-table.component.spec.ts b/console/src/app/modules/smtp-table/smtp-table.component.spec.ts index 8095d73255..fe4719482c 100644 --- a/console/src/app/modules/smtp-table/smtp-table.component.spec.ts +++ b/console/src/app/modules/smtp-table/smtp-table.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { IdpTableComponent } from './smtp-table.component'; +import { SMTPTableComponent } from './smtp-table.component'; describe('UserTableComponent', () => { - let component: IdpTableComponent; - let fixture: ComponentFixture; + let component: SMTPTableComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [IdpTableComponent], + declarations: [SMTPTableComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(IdpTableComponent); + fixture = TestBed.createComponent(SMTPTableComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/pages/actions/actions.component.html b/console/src/app/pages/actions/actions.component.html index 4c55308ced..af936e2351 100644 --- a/console/src/app/pages/actions/actions.component.html +++ b/console/src/app/pages/actions/actions.component.html @@ -6,6 +6,9 @@ info_outline + + {{ 'DESCRIPTIONS.ACTIONS.ACTIONSTWO_NOTE' | translate }} +

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

= new Subject(); + protected maxActions: number | null = null; + protected ActionState = ActionState; constructor( private mgmtService: ManagementService, breadcrumbService: BreadcrumbService, private dialog: MatDialog, private toast: ToastService, + destroyRef: DestroyRef, ) { const bread: Breadcrumb = { type: BreadcrumbType.ORG, @@ -45,31 +45,24 @@ export class ActionsComponent implements OnDestroy { }; breadcrumbService.setBreadcrumb([bread]); - this.getFlowTypes(); + this.getFlowTypes().then(); - this.typeControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { + this.typeControl.valueChanges.pipe(takeUntilDestroyed(destroyRef)).subscribe((value) => { this.loadFlow((value as FlowType.AsObject).id); }); } - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - private getFlowTypes(): Promise { - return this.mgmtService - .listFlowTypes() - .then((resp) => { - this.typesForSelection = resp.resultList; - if (!this.flow && resp.resultList[0]) { - const type = resp.resultList[0]; - this.typeControl.setValue(type); - } - }) - .catch((error: any) => { - this.toast.showError(error); - }); + private async getFlowTypes(): Promise { + try { + let resp = await this.mgmtService.listFlowTypes(); + this.typesForSelection = resp.resultList; + if (!this.flow && resp.resultList[0]) { + const type = resp.resultList[0]; + this.typeControl.setValue(type); + } + } catch (error) { + this.toast.showError(error); + } } private loadFlow(id: string) { @@ -106,7 +99,7 @@ export class ActionsComponent implements OnDestroy { }); } - public openAddTrigger(flow: FlowType.AsObject, trigger?: TriggerType.AsObject): void { + protected openAddTrigger(flow: FlowType.AsObject, trigger?: TriggerType.AsObject): void { const dialogRef = this.dialog.open(AddFlowDialogComponent, { data: { flowType: flow, @@ -119,7 +112,7 @@ export class ActionsComponent implements OnDestroy { if (req) { this.mgmtService .setTriggerActions(req.getActionIdsList(), req.getFlowType(), req.getTriggerType()) - .then((resp) => { + .then(() => { this.toast.showInfo('FLOWS.FLOWCHANGED', true); this.loadFlow(flow.id); }) @@ -157,7 +150,7 @@ export class ActionsComponent implements OnDestroy { } } - public removeTriggerActionsList(index: number) { + protected removeTriggerActionsList(index: number) { if (this.flow.type && this.flow.triggerActionsList && this.flow.triggerActionsList[index]) { const dialogRef = this.dialog.open(WarnDialogComponent, { data: { diff --git a/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.spec.ts b/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.spec.ts index f4b79bee55..b7c7069728 100644 --- a/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.spec.ts +++ b/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AddKeyDialogComponent } from './add-key-dialog.component'; +import { AddActionDialogComponent } from './add-action-dialog.component'; describe('AddKeyDialogComponent', () => { - let component: AddKeyDialogComponent; - let fixture: ComponentFixture; + let component: AddActionDialogComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [AddKeyDialogComponent], + declarations: [AddActionDialogComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(AddKeyDialogComponent); + fixture = TestBed.createComponent(AddActionDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.spec.ts b/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.spec.ts index f4b79bee55..ca9b7c4507 100644 --- a/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.spec.ts +++ b/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AddKeyDialogComponent } from './add-key-dialog.component'; +import { AddFlowDialogComponent } from './add-flow-dialog.component'; describe('AddKeyDialogComponent', () => { - let component: AddKeyDialogComponent; - let fixture: ComponentFixture; + let component: AddFlowDialogComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [AddKeyDialogComponent], + declarations: [AddFlowDialogComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(AddKeyDialogComponent); + fixture = TestBed.createComponent(AddFlowDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/pages/instance/instance.component.ts b/console/src/app/pages/instance/instance.component.ts index 546553132d..e52cdd7198 100644 --- a/console/src/app/pages/instance/instance.component.ts +++ b/console/src/app/pages/instance/instance.component.ts @@ -42,8 +42,6 @@ import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { EnvironmentService } from 'src/app/services/environment.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { NewFeatureService } from '../../services/new-feature.service'; -import { withLatestFromSynchronousFix } from '../../utils/withLatestFromSynchronousFix'; @Component({ selector: 'cnsl-instance', templateUrl: './instance.component.html', @@ -106,7 +104,6 @@ export class InstanceComponent { private readonly envService: EnvironmentService, activatedRoute: ActivatedRoute, private readonly destroyRef: DestroyRef, - private readonly featureService: NewFeatureService, ) { this.loadMembers(); @@ -139,32 +136,7 @@ export class InstanceComponent { } private getSettingsList(): Observable { - const features$ = this.getFeatures().pipe(shareReplay({ refCount: true, bufferSize: 1 })); - - const actionsEnabled$ = features$.pipe(map((features) => features?.actions?.enabled)); - - return this.authService - .isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin || []) - .pipe( - withLatestFromSynchronousFix(actionsEnabled$), - map(([settings, actionsEnabled]) => - settings - .filter((setting) => actionsEnabled || setting.id !== ACTIONS.id) - .filter((setting) => actionsEnabled || setting.id !== ACTIONS_TARGETS.id), - ), - ); - } - - private getFeatures() { - return defer(() => this.featureService.getInstanceFeatures()).pipe( - timeout(1000), - catchError((error) => { - if (!(error instanceof TimeoutError)) { - this.toast.showError(error); - } - return of(undefined); - }), - ); + return this.authService.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin || []); } public loadMembers(): void { diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.ts b/console/src/app/pages/orgs/org-detail/org-detail.component.ts index 0de6696ac3..39514d33d3 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.ts +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.ts @@ -1,7 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; -import { Buffer } from 'buffer'; import { BehaviorSubject, from, Observable, of, Subject, takeUntil } from 'rxjs'; import { catchError, finalize, map } from 'rxjs/operators'; import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; @@ -266,10 +265,11 @@ export class OrgDetailComponent implements OnInit, OnDestroy { .listOrgMetadata() .then((resp) => { this.loadingMetadata = false; - this.metadata = resp.resultList.map((md) => { + const decoder = new TextDecoder(); + this.metadata = resp.resultList.map(({ key, value }) => { return { - key: md.key, - value: Buffer.from(md.value as string, 'base64').toString('utf-8'), + key, + value: atob(typeof value === 'string' ? value : decoder.decode(value)), }; }); }) 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 index 9fd765264d..b92c112357 100644 --- 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 @@ -32,6 +32,7 @@ import { withLatestFromSynchronousFix } from 'src/app/utils/withLatestFromSynchr 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'; +import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; type PwdForm = ReturnType; type AuthenticationFactor = @@ -65,6 +66,7 @@ export class UserCreateV2Component implements OnInit { private readonly destroyRef: DestroyRef, private readonly route: ActivatedRoute, protected readonly location: Location, + private readonly authService: GrpcAuthService, ) { this.userForm = this.buildUserForm(); @@ -180,9 +182,12 @@ export class UserCreateV2Component implements OnInit { private async createUserV2Try(authenticationFactor: AuthenticationFactor) { this.loading.set(true); + const org = await this.authService.getActiveOrg(); + const userValues = this.userForm.getRawValue(); const humanReq: MessageInitShape = { + organization: { org: { case: 'orgId', value: org.id } }, username: userValues.username, profile: { givenName: userValues.givenName, diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html index e36ec2bcbb..22a4498090 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html @@ -1,5 +1,5 @@

- {{ 'USER.MFA.DIALOG.ADD_MFA_TITLE' | translate }} {{ data?.number }} + {{ 'USER.MFA.DIALOG.ADD_MFA_TITLE' | translate }}

@@ -7,6 +7,7 @@
-
-
3.0.0 2025-03-31
+ A-10016 + Position precision fixManual Intervention + + 2.65.102025-05-14
## Subscribe to our Mailing List diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 1f45a017ac..43830eafd0 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -201,28 +201,6 @@ module.exports = { runmeLinkLabel: 'Checkout via Runme' }, }, - webpack: { - jsLoader: (isServer) => ({ - loader: require.resolve('swc-loader'), - options: { - jsc: { - parser: { - syntax: 'typescript', - tsx: true, - }, - transform: { - react: { - runtime: 'automatic', - }, - }, - target: 'es2017', - }, - module: { - type: isServer ? 'commonjs' : 'es6', - }, - }, - }), - }, presets: [ [ "classic", @@ -364,6 +342,30 @@ module.exports = { categoryLinkSource: "auto", }, }, + org_v2beta: { + specPath: ".artifacts/openapi/zitadel/org/v2beta/org_service.swagger.json", + outputDir: "docs/apis/resources/org_service_v2beta", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, + project_v2beta: { + specPath: ".artifacts/openapi/zitadel/project/v2beta/project_service.swagger.json", + outputDir: "docs/apis/resources/project_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, + instance_v2: { + specPath: ".artifacts/openapi/zitadel/instance/v2beta/instance_service.swagger.json", + outputDir: "docs/apis/resources/instance_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, }, }, ], @@ -381,4 +383,17 @@ module.exports = { }, ], themes: [ "docusaurus-theme-github-codeblock", "docusaurus-theme-openapi-docs"], + future: { + v4: false, // Disabled because of some problems related to https://github.com/facebook/docusaurus/issues/11040 + experimental_faster: { + swcJsLoader: false, // Disabled because of memory usage > 8GB which is a problem on vercel default runners + swcJsMinimizer: true, + swcHtmlMinimizer : true, + lightningCssMinimizer: true, + mdxCrossCompilerCache: true, + ssgWorkerThreads: false, // Disabled because of some problems related to https://github.com/facebook/docusaurus/issues/11040 + rspackBundler: true, + rspackPersistentCache: true, + }, + }, }; diff --git a/docs/package.json b/docs/package.json index f9636418dd..014a8ec0ca 100644 --- a/docs/package.json +++ b/docs/package.json @@ -6,7 +6,7 @@ "docusaurus": "docusaurus", "start": "docusaurus start", "start:api": "yarn run generate && docusaurus start", - "build": "yarn run generate && NODE_OPTIONS=--max-old-space-size=8192 docusaurus build", + "build": "yarn run generate && docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", @@ -22,33 +22,27 @@ }, "dependencies": { "@bufbuild/buf": "^1.14.0", - "@docusaurus/core": "3.4.0", - "@docusaurus/preset-classic": "3.4.0", - "@docusaurus/theme-mermaid": "3.4.0", - "@docusaurus/theme-search-algolia": "3.4.0", + "@docusaurus/core": "^3.8.0", + "@docusaurus/faster": "^3.8.0", + "@docusaurus/preset-classic": "^3.8.0", + "@docusaurus/theme-mermaid": "^3.8.0", + "@docusaurus/theme-search-algolia": "^3.8.0", "@headlessui/react": "^1.7.4", "@heroicons/react": "^2.0.13", - "@mdx-js/react": "^3.0.0", - "@swc/core": "^1.3.74", "autoprefixer": "^10.4.13", "clsx": "^1.2.1", - "docusaurus-plugin-image-zoom": "^1.0.1", - "docusaurus-plugin-openapi-docs": "3.0.1", + "docusaurus-plugin-image-zoom": "^3.0.1", + "docusaurus-plugin-openapi-docs": "4.4.0", "docusaurus-theme-github-codeblock": "^2.0.2", - "docusaurus-theme-openapi-docs": "3.0.1", + "docusaurus-theme-openapi-docs": "4.4.0", "mdx-mermaid": "^2.0.0", - "mermaid": "^10.9.1", "postcss": "^8.4.31", - "prism-react-renderer": "^2.1.0", "raw-loader": "^4.0.2", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", "react-google-charts": "^5.2.1", - "react-player": "^2.15.1", - "sitemap": "7.1.1", - "swc-loader": "^0.2.3", - "wait-on": "6.0.1" + "react-player": "^2.15.1" }, "browserslist": { "production": [ @@ -63,8 +57,8 @@ ] }, "devDependencies": { - "@docusaurus/module-type-aliases": "3.4.0", - "@docusaurus/types": "3.4.0", + "@docusaurus/module-type-aliases": "^3.8.0", + "@docusaurus/types": "^3.8.0", "tailwindcss": "^3.2.4" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" diff --git a/docs/sidebars.js b/docs/sidebars.js index 92c7a00b2d..d7ebb80f5b 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -10,9 +10,12 @@ const sidebar_api_oidc_service_v2 = require("./docs/apis/resources/oidc_service_ 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_org_service_v2beta = require("./docs/apis/resources/org_service_v2beta/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_project_service_v2 = require("./docs/apis/resources/project_service_v2/sidebar.ts").default const sidebar_api_webkey_service_v2 = require("./docs/apis/resources/webkey_service_v2/sidebar.ts").default +const sidebar_api_instance_service_v2 = require("./docs/apis/resources/instance_service_v2/sidebar.ts").default module.exports = { guides: [ @@ -180,7 +183,6 @@ module.exports = { items: [ "guides/manage/user/reg-create-user", "guides/manage/customize/user-metadata", - "guides/manage/customize/user-schema", "guides/manage/user/scim2", ], }, @@ -608,6 +610,20 @@ module.exports = { }, ], }, + { + type: "category", + label: "Product Information", + collapsed: true, + items: [ + "product/roadmap", + "product/release-cycle", + { + type: "link", + label: "Changelog", + href: "https://zitadel.com/changelog", + }, + ], + }, { type: "category", label: "Support", diff --git a/docs/src/components/benchmark_chart.jsx b/docs/src/components/benchmark_chart.jsx index 4f0d4bc61c..cf93a842ef 100644 --- a/docs/src/components/benchmark_chart.jsx +++ b/docs/src/components/benchmark_chart.jsx @@ -1,11 +1,24 @@ import React from "react"; import Chart from "react-google-charts"; -export function BenchmarkChart(testResults=[], height='500px') { +export function BenchmarkChart({ testResults = [], height = '500px' } = {}) { + if (!Array.isArray(testResults)) { + console.error("BenchmarkChart: testResults is not an array. Received:", testResults); + return

Error: Benchmark data is not available or in the wrong format.

; + } + + if (testResults.length === 0) { + return

No benchmark data to display.

; + } + const dataPerMetric = new Map(); let maxVValue = 0; - JSON.parse(testResults.testResults).forEach((result) => { + testResults.forEach((result) => { + if (!result || typeof result.metric_name === 'undefined') { + console.warn("BenchmarkChart: Skipping invalid result item:", result); + return; + } if (!dataPerMetric.has(result.metric_name)) { dataPerMetric.set(result.metric_name, [ [ @@ -16,17 +29,16 @@ export function BenchmarkChart(testResults=[], height='500px') { ], ]); } - if (result.p99 > maxVValue) { + if (result.p99 !== undefined && result.p99 > maxVValue) { maxVValue = result.p99; } dataPerMetric.get(result.metric_name).push([ - new Date(result.timestamp), + result.timestamp ? new Date(result.timestamp) : null, result.p50, result.p95, result.p99, ]); }); - const options = { legend: { position: 'bottom' }, focusTarget: 'category', @@ -35,17 +47,18 @@ export function BenchmarkChart(testResults=[], height='500px') { }, vAxis: { title: 'latency (ms)', - maxValue: maxVValue, + maxValue: maxVValue > 0 ? maxVValue : undefined, }, title: '' }; const charts = []; dataPerMetric.forEach((data, metric) => { - const opt = Object.create(options); + const opt = { ...options }; opt.title = metric; charts.push( No chart data could be generated.

; + } - return (charts); + return <>{charts}; } \ No newline at end of file diff --git a/docs/src/components/pii_table.jsx b/docs/src/components/pii_table.jsx new file mode 100644 index 0000000000..5075e1d2ca --- /dev/null +++ b/docs/src/components/pii_table.jsx @@ -0,0 +1,106 @@ +import React from "react"; + +export function PiiTable() { + + const pii = [ + { + type: "Basic data", + examples: [ + 'Names', + 'Email addresses', + 'User names' + ], + subjects: "All users as uploaded by Customer." + }, + { + type: "Login data", + examples: [ + 'Randomly generated ID', + 'Passwords', + 'Public keys / certificates ("FIDO2", "U2F", "x509", ...)', + 'User names or identifiers of external login providers', + 'Phone numbers', + ], + subjects: "All users as uploaded and feature use by Customer." + }, + { + type: "Profile data", + examples: [ + 'Profile pictures', + 'Gender', + 'Languages', + 'Nicknames or Display names', + 'Phone numbers', + 'Metadata' + ], + subjects: "All users as uploaded by Customer" + }, + { + type: "Communication data", + examples: [ + 'Emails', + 'Chats', + 'Call metadata', + 'Call recording and transcripts', + 'Form submissions', + ], + subjects: "Customers and users who communicate with us directly (e.g. support, chat)." + }, + { + type: "Payment data", + examples: [ + 'Billing address', + 'Payment information', + 'Customer number', + 'Support Customer history', + 'Credit rating information', + ], + subjects: "Customers who use services that require payment. Credit rating information: Only customers who pay by invoice." + }, + { + type: "Analytics data", + examples: [ + 'Usage metrics', + 'User behavior', + 'User journeys (eg, Milestones)', + 'Telemetry data', + 'Client-side anonymized session replay', + ], + subjects: "Customers who use our services." + }, + { + type: "Usage meta data", + examples: [ + 'User agent', + 'IP addresses', + 'Operating system', + 'Time and date', + 'URL', + 'Referrer URL', + 'Accepted Language', + ], + subjects: "All users" + }, + ] + + return ( + + + + + + + { + pii.map((row, rowID) => { + return ( + + + + + + ) + }) + } +
Type of personal dataExamplesAffected data subjects
{row.type}
    {row.examples.map((example) => { return (
  • {example}
  • )})}
{row.subjects}
+ ); +} diff --git a/docs/src/components/subprocessors.jsx b/docs/src/components/subprocessors.jsx deleted file mode 100644 index a6bf10eee8..0000000000 --- a/docs/src/components/subprocessors.jsx +++ /dev/null @@ -1,162 +0,0 @@ -import React from "react"; - -export function SubProcessorTable() { - - const country_list = { - us: "USA", - eu: "EU", - ch: "Switzerland", - fr: "France", - in: "India", - de: "Germany", - ee: "Estonia", - nl: "Netherlands", - ro: "Romania", - } - const processors = [ - { - entity: "Google LLC", - purpose: "Cloud infrastructure provider (Google Cloud), business applications and collaboration (Workspace), Data warehouse services, Content delivery network, DDoS and bot prevention", - hosting: "Region designated by Customer, United States", - country: country_list.us, - enduserdata: "Yes" - }, - { - entity: "Datadog, Inc.", - purpose: "Infrastructure monitoring, log analytics, and alerting", - hosting: country_list.eu, - country: country_list.us, - enduserdata: "Yes (logs)" - }, - { - entity: "Github, Inc.", - purpose: "Source code management, code scanning, dependency management, security advisory, issue management, continuous integration", - hosting: country_list.us, - country: country_list.us, - enduserdata: false - }, - { - entity: "Stripe Payments Europe, Ltd.", - purpose: "Subscription management, payment process", - hosting: country_list.us, - country: country_list.us, - enduserdata: false - }, - { - entity: "Bexio AG", - purpose: "Customer management, payment process", - hosting: country_list.ch, - country: country_list.ch, - enduserdata: false - }, - { - entity: "Mailjet SAS", - purpose: "Marketing automation", - hosting: country_list.eu, - country: country_list.fr, - enduserdata: false - }, - { - entity: "Postmark (AC PM LLC)", - purpose: "Transactional mails, if no customer owned SMTP service is configured", - hosting: country_list.us, - country: country_list.us, - enduserdata: "Yes (opt-out)" - }, - { - entity: "Vercel, Inc.", - purpose: "Website hosting", - hosting: country_list.us, - country: country_list.us, - enduserdata: false - }, - { - entity: "Agolia SAS", - purpose: "Documentation search engine (zitadel.com/docs)", - hosting: country_list.us, - country: country_list.in, - enduserdata: false - }, - { - entity: "Discord Netherlands BV", - purpose: "Community chat (zitadel.com/chat)", - hosting: country_list.us, - country: country_list.us, - enduserdata: false - }, - { - entity: "Statuspal", - purpose: "ZITADEL Cloud service status announcements", - hosting: country_list.us, - country: country_list.de, - enduserdata: false - }, - { - entity: "Plausible Insights OÜ", - purpose: "Privacy-friendly web analytics", - hosting: country_list.de, - country: country_list.ee, - enduserdata: false, - dpa: 'https://plausible.io/dpa' - }, - { - entity: "Twillio Inc.", - purpose: "Messaging platform for SMS", - hosting: country_list.us, - country: country_list.us, - enduserdata: "Yes (opt-out)" - }, - { - entity: "Mohlmann Solutions SRL", - purpose: "Global payroll", - hosting: undefined, - country: country_list.ro, - enduserdata: false - }, - { - entity: "Remote Europe Holding, B.V.", - purpose: "Global payroll", - hosting: undefined, - country: country_list.nl, - enduserdata: false - }, - { - entity: "HubSpot Inc.", - purpose: "Customer and sales management, Marketing automation, Support requests", - hosting: country_list.eu, - country: country_list.us, - enduserdata: false - }, - ] - - return ( - - - - - - - - - { - processors - .sort((a, b) => { - if (a.entity < b.entity) return -1 - if (a.entity > b.entity) return 1 - else return 0 - }) - .map((processor, rowID) => { - return ( - - - - - - - - ) - }) - } -
Entity namePurposeEnd-user dataHosting locationCountry of registration
{processor.entity}{processor.purpose}{processor.enduserdata ? processor.enduserdata : 'No'}{processor.hosting ? processor.hosting : 'n/a'}{processor.country}
- ); -} diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 50035f2541..11d4208695 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -641,3 +641,7 @@ p strong { .zitadel-lifecycle-deprecated { text-decoration: line-through; } + +table#zitadel-versions td { + vertical-align: top; +} \ No newline at end of file diff --git a/docs/static/img/go/api-PAT_creation.png b/docs/static/img/go/api-PAT_creation.png new file mode 100644 index 0000000000..b8d2032f05 Binary files /dev/null and b/docs/static/img/go/api-PAT_creation.png differ diff --git a/docs/static/img/go/api-PAT_view.png b/docs/static/img/go/api-PAT_view.png new file mode 100644 index 0000000000..c516f2ae2f Binary files /dev/null and b/docs/static/img/go/api-PAT_view.png differ diff --git a/docs/static/img/go/api-app_details.png b/docs/static/img/go/api-app_details.png new file mode 100644 index 0000000000..730152e937 Binary files /dev/null and b/docs/static/img/go/api-app_details.png differ diff --git a/docs/static/img/go/api-create-auth.png b/docs/static/img/go/api-create-auth.png deleted file mode 100644 index f16980baa9..0000000000 Binary files a/docs/static/img/go/api-create-auth.png and /dev/null differ diff --git a/docs/static/img/go/api-create-key.png b/docs/static/img/go/api-create-key.png deleted file mode 100644 index 200b5f5d12..0000000000 Binary files a/docs/static/img/go/api-create-key.png and /dev/null differ diff --git a/docs/static/img/go/api-create.png b/docs/static/img/go/api-create.png deleted file mode 100644 index 1c21cf0706..0000000000 Binary files a/docs/static/img/go/api-create.png and /dev/null differ diff --git a/docs/static/img/go/api-create_application.png b/docs/static/img/go/api-create_application.png new file mode 100644 index 0000000000..c074d0687f Binary files /dev/null and b/docs/static/img/go/api-create_application.png differ diff --git a/docs/static/img/go/api-create_service_user.png b/docs/static/img/go/api-create_service_user.png new file mode 100644 index 0000000000..6af66bcbca Binary files /dev/null and b/docs/static/img/go/api-create_service_user.png differ diff --git a/docs/static/img/go/api-download_key.png b/docs/static/img/go/api-download_key.png new file mode 100644 index 0000000000..ce2a5fa8cf Binary files /dev/null and b/docs/static/img/go/api-download_key.png differ diff --git a/docs/static/img/go/api-expiration_date.png b/docs/static/img/go/api-expiration_date.png new file mode 100644 index 0000000000..826a87ecd6 Binary files /dev/null and b/docs/static/img/go/api-expiration_date.png differ diff --git a/docs/static/img/go/api-new_key.png b/docs/static/img/go/api-new_key.png new file mode 100644 index 0000000000..f499194b88 Binary files /dev/null and b/docs/static/img/go/api-new_key.png differ diff --git a/docs/static/img/go/api-select_framework.png b/docs/static/img/go/api-select_framework.png new file mode 100644 index 0000000000..aa7f112fe9 Binary files /dev/null and b/docs/static/img/go/api-select_framework.png differ diff --git a/docs/static/img/go/api-select_jwt.png b/docs/static/img/go/api-select_jwt.png new file mode 100644 index 0000000000..161c096b17 Binary files /dev/null and b/docs/static/img/go/api-select_jwt.png differ diff --git a/docs/static/img/go/api-service_user_panel.png b/docs/static/img/go/api-service_user_panel.png new file mode 100644 index 0000000000..d4e9b5beff Binary files /dev/null and b/docs/static/img/go/api-service_user_panel.png differ diff --git a/docs/static/img/guides/jwt_idp.png b/docs/static/img/guides/jwt_idp.png index 73d5353521..218996aef7 100644 Binary files a/docs/static/img/guides/jwt_idp.png and b/docs/static/img/guides/jwt_idp.png differ diff --git a/docs/static/img/product/release-cycle.png b/docs/static/img/product/release-cycle.png new file mode 100644 index 0000000000..7017566ab7 Binary files /dev/null and b/docs/static/img/product/release-cycle.png differ diff --git a/docs/yarn.lock b/docs/yarn.lock index ad31e03b5e..70f2de1f05 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -2,158 +2,153 @@ # yarn lockfile v1 -"@algolia/autocomplete-core@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz#1d56482a768c33aae0868c8533049e02e8961be7" - integrity sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw== +"@algolia/autocomplete-core@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.17.9.tgz#83374c47dc72482aa45d6b953e89377047f0dcdc" + integrity sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ== dependencies: - "@algolia/autocomplete-plugin-algolia-insights" "1.9.3" - "@algolia/autocomplete-shared" "1.9.3" + "@algolia/autocomplete-plugin-algolia-insights" "1.17.9" + "@algolia/autocomplete-shared" "1.17.9" -"@algolia/autocomplete-plugin-algolia-insights@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz#9b7f8641052c8ead6d66c1623d444cbe19dde587" - integrity sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg== +"@algolia/autocomplete-plugin-algolia-insights@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.9.tgz#74c86024d09d09e8bfa3dd90b844b77d9f9947b6" + integrity sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ== dependencies: - "@algolia/autocomplete-shared" "1.9.3" + "@algolia/autocomplete-shared" "1.17.9" -"@algolia/autocomplete-preset-algolia@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz#64cca4a4304cfcad2cf730e83067e0c1b2f485da" - integrity sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA== +"@algolia/autocomplete-preset-algolia@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.9.tgz#911f3250544eb8ea4096fcfb268f156b085321b5" + integrity sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ== dependencies: - "@algolia/autocomplete-shared" "1.9.3" + "@algolia/autocomplete-shared" "1.17.9" -"@algolia/autocomplete-shared@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz#2e22e830d36f0a9cf2c0ccd3c7f6d59435b77dfa" - integrity sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ== +"@algolia/autocomplete-shared@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.9.tgz#5f38868f7cb1d54b014b17a10fc4f7e79d427fa8" + integrity sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ== -"@algolia/cache-browser-local-storage@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.23.3.tgz#0cc26b96085e1115dac5fcb9d826651ba57faabc" - integrity sha512-vRHXYCpPlTDE7i6UOy2xE03zHF2C8MEFjPN2v7fRbqVpcOvAUQK81x3Kc21xyb5aSIpYCjWCZbYZuz8Glyzyyg== +"@algolia/client-abtesting@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.25.0.tgz#012204f1614e1a71366fb1e117c8f195186ff081" + integrity sha512-1pfQulNUYNf1Tk/svbfjfkLBS36zsuph6m+B6gDkPEivFmso/XnRgwDvjAx80WNtiHnmeNjIXdF7Gos8+OLHqQ== dependencies: - "@algolia/cache-common" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/cache-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.23.3.tgz#3bec79092d512a96c9bfbdeec7cff4ad36367166" - integrity sha512-h9XcNI6lxYStaw32pHpB1TMm0RuxphF+Ik4o7tcQiodEdpKK+wKufY6QXtba7t3k8eseirEMVB83uFFF3Nu54A== - -"@algolia/cache-in-memory@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.23.3.tgz#3945f87cd21ffa2bec23890c85305b6b11192423" - integrity sha512-yvpbuUXg/+0rbcagxNT7un0eo3czx2Uf0y4eiR4z4SD7SiptwYTpbuS0IHxcLHG3lq22ukx1T6Kjtk/rT+mqNg== +"@algolia/client-analytics@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.25.0.tgz#eba015bfafb3dbb82712c9160a00717a5974ff71" + integrity sha512-AFbG6VDJX/o2vDd9hqncj1B6B4Tulk61mY0pzTtzKClyTDlNP0xaUiEKhl6E7KO9I/x0FJF5tDCm0Hn6v5x18A== dependencies: - "@algolia/cache-common" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/client-account@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.23.3.tgz#8751bbf636e6741c95e7c778488dee3ee430ac6f" - integrity sha512-hpa6S5d7iQmretHHF40QGq6hz0anWEHGlULcTIT9tbUssWUriN9AUXIFQ8Ei4w9azD0hc1rUok9/DeQQobhQMA== - dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/transporter" "4.23.3" +"@algolia/client-common@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.25.0.tgz#2def8947efe849266057d92f67d1b8d83de0c005" + integrity sha512-il1zS/+Rc6la6RaCdSZ2YbJnkQC6W1wiBO8+SH+DE6CPMWBU6iDVzH0sCKSAtMWl9WBxoN6MhNjGBnCv9Yy2bA== -"@algolia/client-analytics@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.23.3.tgz#f88710885278fe6fb6964384af59004a5a6f161d" - integrity sha512-LBsEARGS9cj8VkTAVEZphjxTjMVCci+zIIiRhpFun9jGDUlS1XmhCW7CTrnaWeIuCQS/2iPyRqSy1nXPjcBLRA== +"@algolia/client-insights@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.25.0.tgz#b87df8614b96c4cc9c9aa7765cce07fa70864fa8" + integrity sha512-blbjrUH1siZNfyCGeq0iLQu00w3a4fBXm0WRIM0V8alcAPo7rWjLbMJMrfBtzL9X5ic6wgxVpDADXduGtdrnkw== dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/client-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.23.3.tgz#891116aa0db75055a7ecc107649f7f0965774704" - integrity sha512-l6EiPxdAlg8CYhroqS5ybfIczsGUIAC47slLPOMDeKSVXYG1n0qGiz4RjAHLw2aD0xzh2EXZ7aRguPfz7UKDKw== +"@algolia/client-personalization@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.25.0.tgz#74b041f0e7d91e1009c131c8d716c34e4d45c30f" + integrity sha512-aywoEuu1NxChBcHZ1pWaat0Plw7A8jDMwjgRJ00Mcl7wGlwuPt5dJ/LTNcg3McsEUbs2MBNmw0ignXBw9Tbgow== dependencies: - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/client-personalization@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.23.3.tgz#35fa8e5699b0295fbc400a8eb211dc711e5909db" - integrity sha512-3E3yF3Ocr1tB/xOZiuC3doHQBQ2zu2MPTYZ0d4lpfWads2WTKG7ZzmGnsHmm63RflvDeLK/UVx7j2b3QuwKQ2g== +"@algolia/client-query-suggestions@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.25.0.tgz#e92d935d9e2994f790d43c64d3518d81070a3888" + integrity sha512-a/W2z6XWKjKjIW1QQQV8PTTj1TXtaKx79uR3NGBdBdGvVdt24KzGAaN7sCr5oP8DW4D3cJt44wp2OY/fZcPAVA== dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/client-search@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.23.3.tgz#a3486e6af13a231ec4ab43a915a1f318787b937f" - integrity sha512-P4VAKFHqU0wx9O+q29Q8YVuaowaZ5EM77rxfmGnkHUJggh28useXQdopokgwMeYw2XUht49WX5RcTQ40rZIabw== +"@algolia/client-search@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.25.0.tgz#dc38ca1015f2f4c9f5053a4517f96fb28a2117f8" + integrity sha512-9rUYcMIBOrCtYiLX49djyzxqdK9Dya/6Z/8sebPn94BekT+KLOpaZCuc6s0Fpfq7nx5J6YY5LIVFQrtioK9u0g== dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" "@algolia/events@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== -"@algolia/logger-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.23.3.tgz#35c6d833cbf41e853a4f36ba37c6e5864920bfe9" - integrity sha512-y9kBtmJwiZ9ZZ+1Ek66P0M68mHQzKRxkW5kAAXYN/rdzgDN0d2COsViEFufxJ0pb45K4FRcfC7+33YB4BLrZ+g== - -"@algolia/logger-console@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.23.3.tgz#30f916781826c4db5f51fcd9a8a264a06e136985" - integrity sha512-8xoiseoWDKuCVnWP8jHthgaeobDLolh00KJAdMe9XPrWPuf1by732jSpgy2BlsLTaT9m32pHI8CRfrOqQzHv3A== +"@algolia/ingestion@1.25.0": + version "1.25.0" + resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.25.0.tgz#4d13c56dda0a05c7bacb0e3ef5866292dfd86ed5" + integrity sha512-jJeH/Hk+k17Vkokf02lkfYE4A+EJX+UgnMhTLR/Mb+d1ya5WhE+po8p5a/Nxb6lo9OLCRl6w3Hmk1TX1e9gVbQ== dependencies: - "@algolia/logger-common" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/recommend@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-4.23.3.tgz#53d4f194d22d9c72dc05f3f7514c5878f87c5890" - integrity sha512-9fK4nXZF0bFkdcLBRDexsnGzVmu4TSYZqxdpgBW2tEyfuSSY54D4qSRkLmNkrrz4YFvdh2GM1gA8vSsnZPR73w== +"@algolia/monitoring@1.25.0": + version "1.25.0" + resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.25.0.tgz#d59360cfe556338519d05a9d8107147e9dbcb020" + integrity sha512-Ls3i1AehJ0C6xaHe7kK9vPmzImOn5zBg7Kzj8tRYIcmCWVyuuFwCIsbuIIz/qzUf1FPSWmw0TZrGeTumk2fqXg== dependencies: - "@algolia/cache-browser-local-storage" "4.23.3" - "@algolia/cache-common" "4.23.3" - "@algolia/cache-in-memory" "4.23.3" - "@algolia/client-common" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/logger-common" "4.23.3" - "@algolia/logger-console" "4.23.3" - "@algolia/requester-browser-xhr" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/requester-node-http" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/requester-browser-xhr@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.23.3.tgz#9e47e76f60d540acc8b27b4ebc7a80d1b41938b9" - integrity sha512-jDWGIQ96BhXbmONAQsasIpTYWslyjkiGu0Quydjlowe+ciqySpiDUrJHERIRfELE5+wFc7hc1Q5hqjGoV7yghw== +"@algolia/recommend@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.25.0.tgz#b96f12c85aa74a0326982c7801fcd4a610b420f4" + integrity sha512-79sMdHpiRLXVxSjgw7Pt4R1aNUHxFLHiaTDnN2MQjHwJ1+o3wSseb55T9VXU4kqy3m7TUme3pyRhLk5ip/S4Mw== dependencies: - "@algolia/requester-common" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/requester-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.23.3.tgz#7dbae896e41adfaaf1d1fa5f317f83a99afb04b3" - integrity sha512-xloIdr/bedtYEGcXCiF2muajyvRhwop4cMZo+K2qzNht0CMzlRkm8YsDdj5IaBhshqfgmBb3rTg4sL4/PpvLYw== - -"@algolia/requester-node-http@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.23.3.tgz#c9f94a5cb96a15f48cea338ab6ef16bbd0ff989f" - integrity sha512-zgu++8Uj03IWDEJM3fuNl34s746JnZOWn1Uz5taV1dFyJhVM/kTNw9Ik7YJWiUNHJQXcaD8IXD1eCb0nq/aByA== +"@algolia/requester-browser-xhr@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.25.0.tgz#c194fa5f49206b9343e6646c41bfbca2a3f2ac54" + integrity sha512-JLaF23p1SOPBmfEqozUAgKHQrGl3z/Z5RHbggBu6s07QqXXcazEsub5VLonCxGVqTv6a61AAPr8J1G5HgGGjEw== dependencies: - "@algolia/requester-common" "4.23.3" + "@algolia/client-common" "5.25.0" -"@algolia/transporter@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.23.3.tgz#545b045b67db3850ddf0bbecbc6c84ff1f3398b7" - integrity sha512-Wjl5gttqnf/gQKJA+dafnD0Y6Yw97yvfY8R9h0dQltX1GXTgNs1zWgvtWW0tHl1EgMdhAyw189uWiZMnL3QebQ== +"@algolia/requester-fetch@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.25.0.tgz#231a2d0da2397d141f80b8f28e2cb6e3d219d38d" + integrity sha512-rtzXwqzFi1edkOF6sXxq+HhmRKDy7tz84u0o5t1fXwz0cwx+cjpmxu/6OQKTdOJFS92JUYHsG51Iunie7xbqfQ== dependencies: - "@algolia/cache-common" "4.23.3" - "@algolia/logger-common" "4.23.3" - "@algolia/requester-common" "4.23.3" + "@algolia/client-common" "5.25.0" + +"@algolia/requester-node-http@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.25.0.tgz#0ce13c550890de21c558b04381535d2d245a3725" + integrity sha512-ZO0UKvDyEFvyeJQX0gmZDQEvhLZ2X10K+ps6hViMo1HgE2V8em00SwNsQ+7E/52a+YiBkVWX61pJJJE44juDMQ== + dependencies: + "@algolia/client-common" "5.25.0" "@alloc/quick-lru@^5.2.0": version "5.2.0" @@ -168,6 +163,19 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" +"@antfu/install-pkg@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz#78fa036be1a6081b5a77a5cf59f50c7752b6ba26" + integrity sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ== + dependencies: + package-manager-detector "^1.3.0" + tinyexec "^1.0.1" + +"@antfu/utils@^8.1.0": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-8.1.1.tgz#95b1947d292a9a2efffba2081796dcaa05ecedfb" + integrity sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ== + "@apidevtools/json-schema-ref-parser@^11.5.4": version "11.6.4" resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.4.tgz#0f3e02302f646471d621a8850e6a346d63c8ebd4" @@ -177,7 +185,7 @@ "@types/json-schema" "^7.0.15" js-yaml "^4.1.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.24.7", "@babel/code-frame@^7.8.3": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== @@ -194,12 +202,26 @@ js-tokens "^4.0.0" picocolors "^1.0.0" +"@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@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" integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw== -"@babel/core@^7.21.3", "@babel/core@^7.23.3": +"@babel/compat-data@^7.27.2": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.27.3.tgz#cc49c2ac222d69b889bf34c795f537c0c6311111" + integrity sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw== + +"@babel/core@^7.21.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4" integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g== @@ -220,7 +242,28 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.23.3", "@babel/generator@^7.24.7": +"@babel/core@^7.25.9": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.27.3.tgz#d7d05502bccede3cab36373ed142e6a1df554c2f" + integrity sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.27.3" + "@babel/helpers" "^7.27.3" + "@babel/parser" "^7.27.3" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.27.3" + "@babel/types" "^7.27.3" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d" integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA== @@ -230,6 +273,17 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" +"@babel/generator@^7.25.9", "@babel/generator@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.3.tgz#ef1c0f7cfe3b5fc8cbb9f6cc69f93441a68edefc" + integrity sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q== + dependencies: + "@babel/parser" "^7.27.3" + "@babel/types" "^7.27.3" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" @@ -237,6 +291,13 @@ dependencies: "@babel/types" "^7.24.7" +"@babel/helper-annotate-as-pure@^7.27.1": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== + dependencies: + "@babel/types" "^7.27.3" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz#37d66feb012024f2422b762b9b2a7cfe27c7fba3" @@ -256,6 +317,17 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz#2eaed36b3a1c11c53bdf80d53838b293c52f5b3b" @@ -271,6 +343,19 @@ "@babel/helper-split-export-declaration" "^7.24.7" semver "^6.3.1" +"@babel/helper-create-class-features-plugin@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz#5bee4262a6ea5ddc852d0806199eb17ca3de9281" + integrity sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.27.1" + semver "^6.3.1" + "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz#be4f435a80dc2b053c76eeb4b7d16dd22cfc89da" @@ -280,6 +365,15 @@ regexpu-core "^5.3.1" semver "^6.3.1" +"@babel/helper-create-regexp-features-plugin@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz#05b0882d97ba1d4d03519e4bce615d70afa18c53" + integrity sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + regexpu-core "^6.2.0" + semver "^6.3.1" + "@babel/helper-define-polyfill-provider@^0.6.1", "@babel/helper-define-polyfill-provider@^0.6.2": version "0.6.2" resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" @@ -291,6 +385,17 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" +"@babel/helper-define-polyfill-provider@^0.6.3": + version "0.6.4" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz#15e8746368bfa671785f5926ff74b3064c291fab" + integrity sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + "@babel/helper-environment-visitor@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" @@ -321,6 +426,14 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helper-member-expression-to-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44" + integrity sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helper-module-imports@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" @@ -329,6 +442,14 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helper-module-transforms@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz#31b6c9a2930679498db65b685b1698bfd6c7daf8" @@ -340,6 +461,15 @@ "@babel/helper-split-export-declaration" "^7.24.7" "@babel/helper-validator-identifier" "^7.24.7" +"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz#db0bbcfba5802f9ef7870705a7ef8788508ede02" + integrity sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.3" + "@babel/helper-optimise-call-expression@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" @@ -347,11 +477,23 @@ dependencies: "@babel/types" "^7.24.7" +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" + "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0" integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg== +"@babel/helper-plugin-utils@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== + "@babel/helper-remap-async-to-generator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz#b3f0f203628522713849d49403f1a414468be4c7" @@ -361,6 +503,15 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-wrap-function" "^7.24.7" +"@babel/helper-remap-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-wrap-function" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/helper-replace-supers@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz#f933b7eed81a1c0265740edc91491ce51250f765" @@ -370,6 +521,15 @@ "@babel/helper-member-expression-to-functions" "^7.24.7" "@babel/helper-optimise-call-expression" "^7.24.7" +"@babel/helper-replace-supers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0" + integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/helper-simple-access@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" @@ -386,6 +546,14 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helper-split-export-declaration@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856" @@ -403,6 +571,11 @@ 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-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + "@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" @@ -413,11 +586,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + "@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" integrity sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw== +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + "@babel/helper-wrap-function@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz#52d893af7e42edca7c6d2c6764549826336aae1f" @@ -428,6 +611,15 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helper-wrap-function@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz#b88285009c31427af318d4fe37651cd62a142409" + integrity sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ== + dependencies: + "@babel/template" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helpers@^7.24.7": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.0.tgz#53d156098defa8243eab0f32fa17589075a1b808" @@ -436,6 +628,14 @@ "@babel/template" "^7.27.0" "@babel/types" "^7.27.0" +"@babel/helpers@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.3.tgz#387d65d279290e22fe7a47a8ffcd2d0c0184edd0" + integrity sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.27.3" + "@babel/highlight@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" @@ -458,6 +658,13 @@ dependencies: "@babel/types" "^7.27.0" +"@babel/parser@^7.27.2", "@babel/parser@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.3.tgz#1b7533f0d908ad2ac545c4d05cbe2fb6dc8cfaaf" + integrity sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw== + dependencies: + "@babel/types" "^7.27.3" + "@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" @@ -466,6 +673,21 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9" + integrity sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz#43f70a6d7efd52370eefbdf55ae03d91b293856d" + integrity sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz#468096ca44bbcbe8fcc570574e12eb1950e18107" @@ -473,6 +695,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz#beb623bd573b8b6f3047bd04c32506adc3e58a72" + integrity sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz#e4eabdd5109acc399b38d7999b2ef66fc2022f89" @@ -482,6 +711,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-transform-optional-chaining" "^7.24.7" +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz#e134a5479eb2ba9c02714e8c1ebf1ec9076124fd" + integrity sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz#71b21bb0286d5810e63a1538aa901c58e87375ec" @@ -490,6 +728,14 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz#bb1c25af34d75115ce229a1de7fa44bf8f955670" + integrity sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" @@ -537,6 +783,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-syntax-import-assertions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz#88894aefd2b03b5ee6ad1562a7c8e1587496aecd" + integrity sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-import-attributes@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz#b4f9ea95a79e6912480c4b626739f86a076624ca" @@ -544,6 +797,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-syntax-import-attributes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" + integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" @@ -565,6 +825,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-syntax-jsx@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" + integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -628,6 +895,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-syntax-typescript@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz#5147d29066a793450f220c63fa3a9431b7e6dd18" + integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" @@ -643,6 +917,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-arrow-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-async-generator-functions@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz#7330a5c50e05181ca52351b8fd01642000c96cfd" @@ -653,6 +934,15 @@ "@babel/helper-remap-async-to-generator" "^7.24.7" "@babel/plugin-syntax-async-generators" "^7.8.4" +"@babel/plugin-transform-async-generator-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz#ca433df983d68e1375398e7ca71bf2a4f6fd89d7" + integrity sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/plugin-transform-async-to-generator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz#72a3af6c451d575842a7e9b5a02863414355bdcc" @@ -662,6 +952,15 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-remap-async-to-generator" "^7.24.7" +"@babel/plugin-transform-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz#9a93893b9379b39466c74474f55af03de78c66e7" + integrity sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/plugin-transform-block-scoped-functions@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz#a4251d98ea0c0f399dafe1a35801eaba455bbf1f" @@ -669,6 +968,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-block-scoped-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz#558a9d6e24cf72802dd3b62a4b51e0d62c0f57f9" + integrity sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-block-scoping@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz#42063e4deb850c7bd7c55e626bf4e7ab48e6ce02" @@ -676,6 +982,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-block-scoping@^7.27.1": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.3.tgz#a21f37e222dc0a7b91c3784fa3bd4edf8d7a6dc1" + integrity sha512-+F8CnfhuLhwUACIJMLWnjz6zvzYM2r0yeIHKlbgfw7ml8rOMJsXNXV/hyRcb3nb493gRs4WvYpQAndWj/qQmkQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-class-properties@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz#256879467b57b0b68c7ddfc5b76584f398cd6834" @@ -684,6 +997,14 @@ "@babel/helper-create-class-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-class-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz#dd40a6a370dfd49d32362ae206ddaf2bb082a925" + integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-class-static-block@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz#c82027ebb7010bc33c116d4b5044fbbf8c05484d" @@ -693,6 +1014,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" +"@babel/plugin-transform-class-static-block@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz#7e920d5625b25bbccd3061aefbcc05805ed56ce4" + integrity sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-classes@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz#4ae6ef43a12492134138c1e45913f7c46c41b4bf" @@ -707,6 +1036,18 @@ "@babel/helper-split-export-declaration" "^7.24.7" globals "^11.1.0" +"@babel/plugin-transform-classes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz#03bb04bea2c7b2f711f0db7304a8da46a85cced4" + integrity sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/traverse" "^7.27.1" + globals "^11.1.0" + "@babel/plugin-transform-computed-properties@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" @@ -715,6 +1056,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/template" "^7.24.7" +"@babel/plugin-transform-computed-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz#81662e78bf5e734a97982c2b7f0a793288ef3caa" + integrity sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/template" "^7.27.1" + "@babel/plugin-transform-destructuring@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz#a097f25292defb6e6cc16d6333a4cfc1e3c72d9e" @@ -722,6 +1071,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-destructuring@^7.27.1", "@babel/plugin-transform-destructuring@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz#3cc8299ed798d9a909f8d66ddeb40849ec32e3b0" + integrity sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-dotall-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz#5f8bf8a680f2116a7207e16288a5f974ad47a7a0" @@ -730,6 +1086,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-dotall-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz#aa6821de864c528b1fecf286f0a174e38e826f4d" + integrity sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-duplicate-keys@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz#dd20102897c9a2324e5adfffb67ff3610359a8ee" @@ -737,6 +1101,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-duplicate-keys@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz#f1fbf628ece18e12e7b32b175940e68358f546d1" + integrity sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz#5043854ca620a94149372e69030ff8cb6a9eb0ec" + integrity sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-dynamic-import@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" @@ -745,6 +1124,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-dynamic-import" "^7.8.3" +"@babel/plugin-transform-dynamic-import@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz#4c78f35552ac0e06aa1f6e3c573d67695e8af5a4" + integrity sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-exponentiation-operator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz#b629ee22645f412024297d5245bce425c31f9b0d" @@ -753,6 +1139,13 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-exponentiation-operator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz#fc497b12d8277e559747f5a3ed868dd8064f83e1" + integrity sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-export-namespace-from@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz#176d52d8d8ed516aeae7013ee9556d540c53f197" @@ -761,6 +1154,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" +"@babel/plugin-transform-export-namespace-from@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz#71ca69d3471edd6daa711cf4dfc3400415df9c23" + integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-for-of@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" @@ -769,6 +1169,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" +"@babel/plugin-transform-for-of@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-function-name@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz#6d8601fbffe665c894440ab4470bc721dd9131d6" @@ -778,6 +1186,15 @@ "@babel/helper-function-name" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-function-name@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== + dependencies: + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/plugin-transform-json-strings@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz#f3e9c37c0a373fee86e36880d45b3664cedaf73a" @@ -786,6 +1203,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-json-strings" "^7.8.3" +"@babel/plugin-transform-json-strings@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz#a2e0ce6ef256376bd527f290da023983527a4f4c" + integrity sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-literals@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz#36b505c1e655151a9d7607799a9988fc5467d06c" @@ -793,6 +1217,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz#a58fb6eda16c9dc8f9ff1c7b1ba6deb7f4694cb0" @@ -801,6 +1232,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" +"@babel/plugin-transform-logical-assignment-operators@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz#890cb20e0270e0e5bebe3f025b434841c32d5baa" + integrity sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-member-expression-literals@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz#3b4454fb0e302e18ba4945ba3246acb1248315df" @@ -808,6 +1246,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-member-expression-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz#37b88ba594d852418e99536f5612f795f23aeaf9" + integrity sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-modules-amd@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz#65090ed493c4a834976a3ca1cde776e6ccff32d7" @@ -816,6 +1261,14 @@ "@babel/helper-module-transforms" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-modules-amd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz#a4145f9d87c2291fe2d05f994b65dba4e3e7196f" + integrity sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-modules-commonjs@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz#9fd5f7fdadee9085886b183f1ad13d1ab260f4ab" @@ -825,6 +1278,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-simple-access" "^7.24.7" +"@babel/plugin-transform-modules-commonjs@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz#8e44ed37c2787ecc23bdc367f49977476614e832" + integrity sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-modules-systemjs@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz#f8012316c5098f6e8dee6ecd58e2bc6f003d0ce7" @@ -835,6 +1296,16 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-validator-identifier" "^7.24.7" +"@babel/plugin-transform-modules-systemjs@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz#00e05b61863070d0f3292a00126c16c0e024c4ed" + integrity sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/plugin-transform-modules-umd@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz#edd9f43ec549099620df7df24e7ba13b5c76efc8" @@ -843,6 +1314,14 @@ "@babel/helper-module-transforms" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-modules-umd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz#63f2cf4f6dc15debc12f694e44714863d34cd334" + integrity sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz#9042e9b856bc6b3688c0c2e4060e9e10b1460923" @@ -851,6 +1330,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz#f32b8f7818d8fc0cc46ee20a8ef75f071af976e1" + integrity sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-new-target@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz#31ff54c4e0555cc549d5816e4ab39241dfb6ab00" @@ -858,6 +1345,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-new-target@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz#259c43939728cad1706ac17351b7e6a7bea1abeb" + integrity sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz#1de4534c590af9596f53d67f52a92f12db984120" @@ -866,6 +1360,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" +"@babel/plugin-transform-nullish-coalescing-operator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz#4f9d3153bf6782d73dd42785a9d22d03197bc91d" + integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-numeric-separator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" @@ -874,6 +1375,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-numeric-separator" "^7.10.4" +"@babel/plugin-transform-numeric-separator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz#614e0b15cc800e5997dadd9bd6ea524ed6c819c6" + integrity sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-object-rest-spread@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz#d13a2b93435aeb8a197e115221cab266ba6e55d6" @@ -884,6 +1392,16 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-transform-parameters" "^7.24.7" +"@babel/plugin-transform-object-rest-spread@^7.27.2": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz#ce130aa73fef828bc3e3e835f9bc6144be3eb1c0" + integrity sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q== + dependencies: + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.27.3" + "@babel/plugin-transform-parameters" "^7.27.1" + "@babel/plugin-transform-object-super@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" @@ -892,6 +1410,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-replace-supers" "^7.24.7" +"@babel/plugin-transform-object-super@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz#1c932cd27bf3874c43a5cac4f43ebf970c9871b5" + integrity sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz#00eabd883d0dd6a60c1c557548785919b6e717b4" @@ -900,6 +1426,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" +"@babel/plugin-transform-optional-catch-binding@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz#84c7341ebde35ccd36b137e9e45866825072a30c" + integrity sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-optional-chaining@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz#b8f6848a80cf2da98a8a204429bec04756c6d454" @@ -909,6 +1442,14 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-syntax-optional-chaining" "^7.8.3" +"@babel/plugin-transform-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz#874ce3c4f06b7780592e946026eb76a32830454f" + integrity sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-parameters@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" @@ -916,6 +1457,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-parameters@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz#80334b54b9b1ac5244155a0c8304a187a618d5a7" + integrity sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-private-methods@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz#e6318746b2ae70a59d023d5cc1344a2ba7a75f5e" @@ -924,6 +1472,14 @@ "@babel/helper-create-class-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-private-methods@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz#fdacbab1c5ed81ec70dfdbb8b213d65da148b6af" + integrity sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-private-property-in-object@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz#4eec6bc701288c1fab5f72e6a4bbc9d67faca061" @@ -934,6 +1490,15 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" +"@babel/plugin-transform-private-property-in-object@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz#4dbbef283b5b2f01a21e81e299f76e35f900fb11" + integrity sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-property-literals@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" @@ -941,6 +1506,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-property-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz#07eafd618800591e88073a0af1b940d9a42c6424" + integrity sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-react-constant-elements@^7.21.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.24.7.tgz#b85e8f240b14400277f106c9c9b585d9acf608a1" @@ -955,6 +1527,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-react-display-name@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.27.1.tgz#43af31362d71f7848cfac0cbc212882b1a16e80f" + integrity sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-react-jsx-development@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz#eaee12f15a93f6496d852509a850085e6361470b" @@ -962,6 +1541,13 @@ dependencies: "@babel/plugin-transform-react-jsx" "^7.24.7" +"@babel/plugin-transform-react-jsx-development@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz#47ff95940e20a3a70e68ad3d4fcb657b647f6c98" + integrity sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.27.1" + "@babel/plugin-transform-react-jsx@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz#17cd06b75a9f0e2bd076503400e7c4b99beedac4" @@ -973,6 +1559,17 @@ "@babel/plugin-syntax-jsx" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/plugin-transform-react-jsx@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz#1023bc94b78b0a2d68c82b5e96aed573bcfb9db0" + integrity sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/plugin-transform-react-pure-annotations@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz#bdd9d140d1c318b4f28b29a00fb94f97ecab1595" @@ -981,6 +1578,14 @@ "@babel/helper-annotate-as-pure" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-react-pure-annotations@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz#339f1ce355eae242e0649f232b1c68907c02e879" + integrity sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-regenerator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" @@ -989,6 +1594,21 @@ "@babel/helper-plugin-utils" "^7.24.7" regenerator-transform "^0.15.2" +"@babel/plugin-transform-regenerator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz#0a471df9213416e44cd66bf67176b66f65768401" + integrity sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-regexp-modifiers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz#df9ba5577c974e3f1449888b70b76169998a6d09" + integrity sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-reserved-words@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" @@ -996,15 +1616,22 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-runtime@^7.22.9": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz#00a5bfaf8c43cf5c8703a8a6e82b59d9c58f38ca" - integrity sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw== +"@babel/plugin-transform-reserved-words@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz#40fba4878ccbd1c56605a4479a3a891ac0274bb4" + integrity sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw== dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-runtime@^7.25.9": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.27.3.tgz#ad35f1eff5ba18a5e23f7270e939fb5a59d3ec0b" + integrity sha512-bA9ZL5PW90YwNgGfjg6U+7Qh/k3zCEQJ06BFgAGRp/yMjw9hP9UGbGPtx3KSOkHGljEPCCxaE+PH4fUR2h1sDw== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.1" + babel-plugin-polyfill-corejs3 "^0.11.0" babel-plugin-polyfill-regenerator "^0.6.1" semver "^6.3.1" @@ -1015,6 +1642,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-shorthand-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-spread@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" @@ -1023,6 +1657,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" +"@babel/plugin-transform-spread@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz#1a264d5fc12750918f50e3fe3e24e437178abb08" + integrity sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-sticky-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" @@ -1030,6 +1672,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-sticky-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-template-literals@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" @@ -1037,6 +1686,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-template-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8" + integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-typeof-symbol@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz#f074be466580d47d6e6b27473a840c9f9ca08fb0" @@ -1044,6 +1700,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-typeof-symbol@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz#70e966bb492e03509cf37eafa6dcc3051f844369" + integrity sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-typescript@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz#b006b3e0094bf0813d505e0c5485679eeaf4a881" @@ -1054,6 +1717,17 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-typescript" "^7.24.7" +"@babel/plugin-transform-typescript@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz#d3bb65598bece03f773111e88cc4e8e5070f1140" + integrity sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-syntax-typescript" "^7.27.1" + "@babel/plugin-transform-unicode-escapes@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz#2023a82ced1fb4971630a2e079764502c4148e0e" @@ -1061,6 +1735,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-unicode-escapes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" + integrity sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz#9073a4cd13b86ea71c3264659590ac086605bbcd" @@ -1069,6 +1750,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-unicode-property-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz#bdfe2d3170c78c5691a3c3be934c8c0087525956" + integrity sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-unicode-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" @@ -1077,6 +1766,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-unicode-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz#d40705d67523803a576e29c63cef6e516b858ed9" @@ -1085,7 +1782,15 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/preset-env@^7.20.2", "@babel/preset-env@^7.22.9": +"@babel/plugin-transform-unicode-sets-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz#6ab706d10f801b5c72da8bb2548561fa04193cd1" + integrity sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/preset-env@^7.20.2": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.7.tgz#ff067b4e30ba4a72f225f12f123173e77b987f37" integrity sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ== @@ -1172,6 +1877,81 @@ core-js-compat "^3.31.0" semver "^6.3.1" +"@babel/preset-env@^7.25.9": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.27.2.tgz#106e6bfad92b591b1f6f76fd4cf13b7725a7bf9a" + integrity sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.27.1" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.27.1" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.27.1" + "@babel/plugin-syntax-import-attributes" "^7.27.1" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.27.1" + "@babel/plugin-transform-async-generator-functions" "^7.27.1" + "@babel/plugin-transform-async-to-generator" "^7.27.1" + "@babel/plugin-transform-block-scoped-functions" "^7.27.1" + "@babel/plugin-transform-block-scoping" "^7.27.1" + "@babel/plugin-transform-class-properties" "^7.27.1" + "@babel/plugin-transform-class-static-block" "^7.27.1" + "@babel/plugin-transform-classes" "^7.27.1" + "@babel/plugin-transform-computed-properties" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.27.1" + "@babel/plugin-transform-dotall-regex" "^7.27.1" + "@babel/plugin-transform-duplicate-keys" "^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-dynamic-import" "^7.27.1" + "@babel/plugin-transform-exponentiation-operator" "^7.27.1" + "@babel/plugin-transform-export-namespace-from" "^7.27.1" + "@babel/plugin-transform-for-of" "^7.27.1" + "@babel/plugin-transform-function-name" "^7.27.1" + "@babel/plugin-transform-json-strings" "^7.27.1" + "@babel/plugin-transform-literals" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.27.1" + "@babel/plugin-transform-member-expression-literals" "^7.27.1" + "@babel/plugin-transform-modules-amd" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-modules-systemjs" "^7.27.1" + "@babel/plugin-transform-modules-umd" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-new-target" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.27.1" + "@babel/plugin-transform-numeric-separator" "^7.27.1" + "@babel/plugin-transform-object-rest-spread" "^7.27.2" + "@babel/plugin-transform-object-super" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + "@babel/plugin-transform-parameters" "^7.27.1" + "@babel/plugin-transform-private-methods" "^7.27.1" + "@babel/plugin-transform-private-property-in-object" "^7.27.1" + "@babel/plugin-transform-property-literals" "^7.27.1" + "@babel/plugin-transform-regenerator" "^7.27.1" + "@babel/plugin-transform-regexp-modifiers" "^7.27.1" + "@babel/plugin-transform-reserved-words" "^7.27.1" + "@babel/plugin-transform-shorthand-properties" "^7.27.1" + "@babel/plugin-transform-spread" "^7.27.1" + "@babel/plugin-transform-sticky-regex" "^7.27.1" + "@babel/plugin-transform-template-literals" "^7.27.1" + "@babel/plugin-transform-typeof-symbol" "^7.27.1" + "@babel/plugin-transform-unicode-escapes" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex" "^7.27.1" + "@babel/plugin-transform-unicode-regex" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.27.1" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.11.0" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.40.0" + semver "^6.3.1" + "@babel/preset-modules@0.1.6-no-external-plugins": version "0.1.6-no-external-plugins" resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" @@ -1181,7 +1961,7 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@^7.18.6", "@babel/preset-react@^7.22.5": +"@babel/preset-react@^7.18.6": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.24.7.tgz#480aeb389b2a798880bf1f889199e3641cbb22dc" integrity sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag== @@ -1193,7 +1973,19 @@ "@babel/plugin-transform-react-jsx-development" "^7.24.7" "@babel/plugin-transform-react-pure-annotations" "^7.24.7" -"@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.22.5": +"@babel/preset-react@^7.25.9": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.27.1.tgz#86ea0a5ca3984663f744be2fd26cb6747c3fd0ec" + integrity sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-transform-react-display-name" "^7.27.1" + "@babel/plugin-transform-react-jsx" "^7.27.1" + "@babel/plugin-transform-react-jsx-development" "^7.27.1" + "@babel/plugin-transform-react-pure-annotations" "^7.27.1" + +"@babel/preset-typescript@^7.21.0": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz#66cd86ea8f8c014855671d5ea9a737139cbbfef1" integrity sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ== @@ -1204,26 +1996,41 @@ "@babel/plugin-transform-modules-commonjs" "^7.24.7" "@babel/plugin-transform-typescript" "^7.24.7" +"@babel/preset-typescript@^7.25.9": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz#190742a6428d282306648a55b0529b561484f912" + integrity sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-typescript" "^7.27.1" + "@babel/regjsgen@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime-corejs3@^7.22.6": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.24.7.tgz#65a99097e4c28e6c3a174825591700cc5abd710e" - integrity sha512-eytSX6JLBY6PVAeQa2bFlDx/7Mmln/gaEpsit5a3WEvjGfiIytEsgAwuIXCPM0xvw0v0cJn3ilq0/TvXrW0kgA== +"@babel/runtime-corejs3@^7.25.9": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.3.tgz#b971a4a0a376171e266629152e74ef50e9931f79" + integrity sha512-ZYcgrwb+dkWNcDlsTe4fH1CMdqMDSJ5lWFd1by8Si2pI54XcQjte/+ViIPqAk7EAWisaUxvQ89grv+bNX2x8zg== dependencies: core-js-pure "^3.30.2" - 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": +"@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.8.4", "@babel/runtime@^7.9.2": 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" +"@babel/runtime@^7.25.9": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.3.tgz#10491113799fb8d77e1d9273384d5d68deeea8f6" + integrity sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw== + "@babel/template@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" @@ -1242,7 +2049,16 @@ "@babel/parser" "^7.27.0" "@babel/types" "^7.27.0" -"@babel/traverse@^7.22.8", "@babel/traverse@^7.24.7": +"@babel/template@^7.27.1", "@babel/template@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + +"@babel/traverse@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" integrity sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA== @@ -1258,6 +2074,19 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.3.tgz#8b62a6c2d10f9d921ba7339c90074708509cffae" + integrity sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.3" + "@babel/parser" "^7.27.3" + "@babel/template" "^7.27.2" + "@babel/types" "^7.27.3" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.21.3", "@babel/types@^7.24.7", "@babel/types@^7.4.4": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2" @@ -1275,10 +2104,18 @@ "@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" - integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== +"@babel/types@^7.27.1", "@babel/types@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.3.tgz#c0257bedf33aad6aad1f406d35c44758321eb3ec" + integrity sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + +"@braintree/sanitize-url@^7.0.4": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz#15e19737d946559289b915e5dad3b4c28407735e" + integrity sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw== "@bufbuild/buf-darwin-arm64@1.33.0": version "1.33.0" @@ -1322,138 +2159,543 @@ "@bufbuild/buf-win32-arm64" "1.33.0" "@bufbuild/buf-win32-x64" "1.33.0" +"@chevrotain/cst-dts-gen@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz#5e0863cc57dc45e204ccfee6303225d15d9d4783" + integrity sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ== + dependencies: + "@chevrotain/gast" "11.0.3" + "@chevrotain/types" "11.0.3" + lodash-es "4.17.21" + +"@chevrotain/gast@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/gast/-/gast-11.0.3.tgz#e84d8880323fe8cbe792ef69ce3ffd43a936e818" + integrity sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q== + dependencies: + "@chevrotain/types" "11.0.3" + lodash-es "4.17.21" + +"@chevrotain/regexp-to-ast@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz#11429a81c74a8e6a829271ce02fc66166d56dcdb" + integrity sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA== + +"@chevrotain/types@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/types/-/types-11.0.3.tgz#f8a03914f7b937f594f56eb89312b3b8f1c91848" + integrity sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ== + +"@chevrotain/utils@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/utils/-/utils-11.0.3.tgz#e39999307b102cff3645ec4f5b3665f5297a2224" + integrity sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ== + "@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== +"@csstools/cascade-layer-name-parser@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz#43f962bebead0052a9fed1a2deeb11f85efcbc72" + integrity sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A== + +"@csstools/color-helpers@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.0.2.tgz#82592c9a7c2b83c293d9161894e2a6471feb97b8" + integrity sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA== + +"@csstools/css-calc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.4.tgz#8473f63e2fcd6e459838dd412401d5948f224c65" + integrity sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ== + +"@csstools/css-color-parser@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz#79fc68864dd43c3b6782d2b3828bc0fa9d085c10" + integrity sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg== + dependencies: + "@csstools/color-helpers" "^5.0.2" + "@csstools/css-calc" "^2.1.4" + +"@csstools/css-parser-algorithms@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz#5755370a9a29abaec5515b43c8b3f2cf9c2e3076" + integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ== + +"@csstools/css-tokenizer@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz#333fedabc3fd1a8e5d0100013731cf19e6a8c5d3" + integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw== + +"@csstools/media-query-list-parser@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz#7aec77bcb89c2da80ef207e73f474ef9e1b3cdf1" + integrity sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ== + +"@csstools/postcss-cascade-layers@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.1.tgz#9640313e64b5e39133de7e38a5aa7f40dc259597" + integrity sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-color-function@^4.0.10": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-4.0.10.tgz#11ad43a66ef2cc794ab826a07df8b5fa9fb47a3a" + integrity sha512-4dY0NBu7NVIpzxZRgh/Q/0GPSz/jLSw0i/u3LTUor0BkQcz/fNhN10mSWBDsL0p9nDb0Ky1PD6/dcGbhACuFTQ== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-color-mix-function@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.10.tgz#8c9d0ccfae5c45a9870dd84807ea2995c7a3a514" + integrity sha512-P0lIbQW9I4ShE7uBgZRib/lMTf9XMjJkFl/d6w4EMNHu2qvQ6zljJGEcBkw/NsBtq/6q3WrmgxSS8kHtPMkK4Q== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-color-mix-variadic-function-arguments@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.0.tgz#0b29cb9b4630d7ed68549db265662d41554a17ed" + integrity sha512-Z5WhouTyD74dPFPrVE7KydgNS9VvnjB8qcdes9ARpCOItb4jTnm7cHp4FhxCRUoyhabD0WVv43wbkJ4p8hLAlQ== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-content-alt-text@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.6.tgz#548862226eac54bab0ee5f1bf3a9981393ab204b" + integrity sha512-eRjLbOjblXq+byyaedQRSrAejKGNAFued+LcbzT+LCL78fabxHkxYjBbxkroONxHHYu2qxhFK2dBStTLPG3jpQ== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-exponential-functions@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz#fc03d1272888cb77e64cc1a7d8a33016e4f05c69" + integrity sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-font-format-keywords@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz#6730836eb0153ff4f3840416cc2322f129c086e6" + integrity sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-gamut-mapping@^2.0.10": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.10.tgz#f518d941231d721dbecf5b41e3c441885ff2f28b" + integrity sha512-QDGqhJlvFnDlaPAfCYPsnwVA6ze+8hhrwevYWlnUeSjkkZfBpcCO42SaUD8jiLlq7niouyLgvup5lh+f1qessg== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-gradients-interpolation-method@^5.0.10": + version "5.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.10.tgz#3146da352c31142a721fdba062ac3a6d11dbbec3" + integrity sha512-HHPauB2k7Oits02tKFUeVFEU2ox/H3OQVrP3fSOKDxvloOikSal+3dzlyTZmYsb9FlY9p5EUpBtz0//XBmy+aw== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-hwb-function@^4.0.10": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.10.tgz#f93f3c457e6440ac37ef9b908feb5d901b417d50" + integrity sha512-nOKKfp14SWcdEQ++S9/4TgRKchooLZL0TUFdun3nI4KPwCjETmhjta1QT4ICQcGVWQTvrsgMM/aLB5We+kMHhQ== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-ic-unit@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.2.tgz#7561e09db65fac8304ceeab9dd3e5c6e43414587" + integrity sha512-lrK2jjyZwh7DbxaNnIUjkeDmU8Y6KyzRBk91ZkI5h8nb1ykEfZrtIVArdIjX4DHMIBGpdHrgP0n4qXDr7OHaKA== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-initial@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz#c385bd9d8ad31ad159edd7992069e97ceea4d09a" + integrity sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg== + +"@csstools/postcss-is-pseudo-class@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.1.tgz#12041448fedf01090dd4626022c28b7f7623f58e" + integrity sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-light-dark-function@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.9.tgz#9fb080188907539734a9d5311d2a1cb82531ef38" + integrity sha512-1tCZH5bla0EAkFAI2r0H33CDnIBeLUaJh1p+hvvsylJ4svsv2wOmJjJn+OXwUZLXef37GYbRIVKX+X+g6m+3CQ== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-logical-float-and-clear@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz#62617564182cf86ab5d4e7485433ad91e4c58571" + integrity sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ== + +"@csstools/postcss-logical-overflow@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz#c6de7c5f04e3d4233731a847f6c62819bcbcfa1d" + integrity sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA== + +"@csstools/postcss-logical-overscroll-behavior@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz#43c03eaecdf34055ef53bfab691db6dc97a53d37" + integrity sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w== + +"@csstools/postcss-logical-resize@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz#4df0eeb1a61d7bd85395e56a5cce350b5dbfdca6" + integrity sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-logical-viewport-units@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz#016d98a8b7b5f969e58eb8413447eb801add16fc" + integrity sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ== + dependencies: + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-media-minmax@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz#184252d5b93155ae526689328af6bdf3fc113987" + integrity sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" + +"@csstools/postcss-media-queries-aspect-ratio-number-values@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz#f485c31ec13d6b0fb5c528a3474334a40eff5f11" + integrity sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" + +"@csstools/postcss-nested-calc@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz#754e10edc6958d664c11cde917f44ba144141c62" + integrity sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-normalize-display-values@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz#ecdde2daf4e192e5da0c6fd933b6d8aff32f2a36" + integrity sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-oklab-function@^4.0.10": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.10.tgz#d4c23c51dd0be45e6dedde22432d7d0003710780" + integrity sha512-ZzZUTDd0fgNdhv8UUjGCtObPD8LYxMH+MJsW9xlZaWTV8Ppr4PtxlHYNMmF4vVWGl0T6f8tyWAKjoI6vePSgAg== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-progressive-custom-properties@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.1.0.tgz#70c8d41b577f4023633b7e3791604e0b7f3775bc" + integrity sha512-YrkI9dx8U4R8Sz2EJaoeD9fI7s7kmeEBfmO+UURNeL6lQI7VxF6sBE+rSqdCBn4onwqmxFdBU3lTwyYb/lCmxA== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-random-function@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz#3191f32fe72936e361dadf7dbfb55a0209e2691e" + integrity sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-relative-color-syntax@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.10.tgz#daa840583969461e1e06b12e9c591e52a790ec86" + integrity sha512-8+0kQbQGg9yYG8hv0dtEpOMLwB9M+P7PhacgIzVzJpixxV4Eq9AUQtQw8adMmAJU1RBBmIlpmtmm3XTRd/T00g== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-scope-pseudo-class@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz#9fe60e9d6d91d58fb5fc6c768a40f6e47e89a235" + integrity sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q== + dependencies: + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-sign-functions@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz#a9ac56954014ae4c513475b3f1b3e3424a1e0c12" + integrity sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-stepped-value-functions@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz#36036f1a0e5e5ee2308e72f3c9cb433567c387b9" + integrity sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-text-decoration-shorthand@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.2.tgz#a3bcf80492e6dda36477538ab8e8943908c9f80a" + integrity sha512-8XvCRrFNseBSAGxeaVTaNijAu+FzUvjwFXtcrynmazGb/9WUdsPCpBX+mHEHShVRq47Gy4peYAoxYs8ltUnmzA== + dependencies: + "@csstools/color-helpers" "^5.0.2" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-trigonometric-functions@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz#3f94ed2e319b57f2c59720b64e4d0a8a6fb8c3b2" + integrity sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-unset-value@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz#7caa981a34196d06a737754864baf77d64de4bba" + integrity sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA== + +"@csstools/selector-resolve-nested@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz#704a9b637975680e025e069a4c58b3beb3e2752a" + integrity sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ== + +"@csstools/selector-specificity@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz#037817b574262134cabd68fc4ec1a454f168407b" + integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw== + +"@csstools/utilities@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/utilities/-/utilities-2.0.0.tgz#f7ff0fee38c9ffb5646d47b6906e0bc8868bde60" + integrity sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ== + "@discoveryjs/json-ext@0.5.7": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@docsearch/css@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.6.0.tgz#0e9f56f704b3a34d044d15fd9962ebc1536ba4fb" - integrity sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ== +"@docsearch/css@3.9.0": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.9.0.tgz#3bc29c96bf024350d73b0cfb7c2a7b71bf251cd5" + integrity sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA== -"@docsearch/react@^3.5.2": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.6.0.tgz#b4f25228ecb7fc473741aefac592121e86dd2958" - integrity sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w== +"@docsearch/react@^3.9.0": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.9.0.tgz#d0842b700c3ee26696786f3c8ae9f10c1a3f0db3" + integrity sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ== dependencies: - "@algolia/autocomplete-core" "1.9.3" - "@algolia/autocomplete-preset-algolia" "1.9.3" - "@docsearch/css" "3.6.0" - algoliasearch "^4.19.1" + "@algolia/autocomplete-core" "1.17.9" + "@algolia/autocomplete-preset-algolia" "1.17.9" + "@docsearch/css" "3.9.0" + algoliasearch "^5.14.2" -"@docusaurus/core@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.4.0.tgz#bdbf1af4b2f25d1bf4a5b62ec6137d84c821cb3c" - integrity sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w== +"@docusaurus/babel@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.8.0.tgz#2f390cc4e588a96ec496d87921e44890899738a6" + integrity sha512-9EJwSgS6TgB8IzGk1L8XddJLhZod8fXT4ULYMx6SKqyCBqCFpVCEjR/hNXXhnmtVM2irDuzYoVLGWv7srG/VOA== dependencies: - "@babel/core" "^7.23.3" - "@babel/generator" "^7.23.3" + "@babel/core" "^7.25.9" + "@babel/generator" "^7.25.9" "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-transform-runtime" "^7.22.9" - "@babel/preset-env" "^7.22.9" - "@babel/preset-react" "^7.22.5" - "@babel/preset-typescript" "^7.22.5" - "@babel/runtime" "^7.22.6" - "@babel/runtime-corejs3" "^7.22.6" - "@babel/traverse" "^7.22.8" - "@docusaurus/cssnano-preset" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - autoprefixer "^10.4.14" - babel-loader "^9.1.3" + "@babel/plugin-transform-runtime" "^7.25.9" + "@babel/preset-env" "^7.25.9" + "@babel/preset-react" "^7.25.9" + "@babel/preset-typescript" "^7.25.9" + "@babel/runtime" "^7.25.9" + "@babel/runtime-corejs3" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@docusaurus/logger" "3.8.0" + "@docusaurus/utils" "3.8.0" babel-plugin-dynamic-import-node "^2.3.3" - boxen "^6.2.1" - chalk "^4.1.2" - chokidar "^3.5.3" + fs-extra "^11.1.1" + tslib "^2.6.0" + +"@docusaurus/bundler@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.8.0.tgz#386f54dca594d81bac6b617c71822e0808d6e2f6" + integrity sha512-Rq4Z/MSeAHjVzBLirLeMcjLIAQy92pF1OI+2rmt18fSlMARfTGLWRE8Vb+ljQPTOSfJxwDYSzsK6i7XloD2rNA== + dependencies: + "@babel/core" "^7.25.9" + "@docusaurus/babel" "3.8.0" + "@docusaurus/cssnano-preset" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + babel-loader "^9.2.1" clean-css "^5.3.2" - cli-table3 "^0.6.3" - combine-promises "^1.1.0" - commander "^5.1.0" copy-webpack-plugin "^11.0.0" - core-js "^3.31.1" css-loader "^6.8.1" css-minimizer-webpack-plugin "^5.0.1" cssnano "^6.1.2" - del "^6.1.1" + file-loader "^6.2.0" + html-minifier-terser "^7.2.0" + mini-css-extract-plugin "^2.9.1" + null-loader "^4.0.1" + postcss "^8.4.26" + postcss-loader "^7.3.3" + postcss-preset-env "^10.1.0" + terser-webpack-plugin "^5.3.9" + tslib "^2.6.0" + url-loader "^4.1.1" + webpack "^5.95.0" + webpackbar "^6.0.1" + +"@docusaurus/core@3.8.0", "@docusaurus/core@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.8.0.tgz#79d5e1084415c8834a8a5cb87162ca13f52fe147" + integrity sha512-c7u6zFELmSGPEP9WSubhVDjgnpiHgDqMh1qVdCB7rTflh4Jx0msTYmMiO91Ez0KtHj4sIsDsASnjwfJ2IZp3Vw== + dependencies: + "@docusaurus/babel" "3.8.0" + "@docusaurus/bundler" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + boxen "^6.2.1" + chalk "^4.1.2" + chokidar "^3.5.3" + cli-table3 "^0.6.3" + combine-promises "^1.1.0" + commander "^5.1.0" + core-js "^3.31.1" detect-port "^1.5.1" escape-html "^1.0.3" eta "^2.2.0" eval "^0.1.8" - file-loader "^6.2.0" + execa "5.1.1" fs-extra "^11.1.1" - html-minifier-terser "^7.2.0" html-tags "^3.3.1" - html-webpack-plugin "^5.5.3" + html-webpack-plugin "^5.6.0" leven "^3.1.0" lodash "^4.17.21" - mini-css-extract-plugin "^2.7.6" + open "^8.4.0" p-map "^4.0.0" - postcss "^8.4.26" - postcss-loader "^7.3.3" prompts "^2.4.2" - react-dev-utils "^12.0.1" - react-helmet-async "^1.3.0" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" react-loadable "npm:@docusaurus/react-loadable@6.0.0" react-loadable-ssr-addon-v5-slorber "^1.0.1" react-router "^5.3.4" react-router-config "^5.1.1" react-router-dom "^5.3.4" - rtl-detect "^1.0.4" semver "^7.5.4" - serve-handler "^6.1.5" - shelljs "^0.8.5" - terser-webpack-plugin "^5.3.9" + serve-handler "^6.1.6" + tinypool "^1.0.2" tslib "^2.6.0" update-notifier "^6.0.2" - url-loader "^4.1.1" - webpack "^5.88.1" - webpack-bundle-analyzer "^4.9.0" - webpack-dev-server "^4.15.1" - webpack-merge "^5.9.0" - webpackbar "^5.0.2" + webpack "^5.95.0" + webpack-bundle-analyzer "^4.10.2" + webpack-dev-server "^4.15.2" + webpack-merge "^6.0.1" -"@docusaurus/cssnano-preset@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.4.0.tgz#dc7922b3bbeabcefc9b60d0161680d81cf72c368" - integrity sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ== +"@docusaurus/cssnano-preset@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.8.0.tgz#a70f19e2995be2299f5ef9c3da3e5d4d5c14bff2" + integrity sha512-UJ4hAS2T0R4WNy+phwVff2Q0L5+RXW9cwlH6AEphHR5qw3m/yacfWcSK7ort2pMMbDn8uGrD38BTm4oLkuuNoQ== dependencies: cssnano-preset-advanced "^6.1.2" postcss "^8.4.38" postcss-sort-media-queries "^5.2.0" tslib "^2.6.0" -"@docusaurus/logger@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.4.0.tgz#8b0ac05c7f3dac2009066e2f964dee8209a77403" - integrity sha512-bZwkX+9SJ8lB9kVRkXw+xvHYSMGG4bpYHKGXeXFvyVc79NMeeBSGgzd4TQLHH+DYeOJoCdl8flrFJVxlZ0wo/Q== +"@docusaurus/faster@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/faster/-/faster-3.8.0.tgz#2814c5ea4f19e10a6cebf9296b6f15f8a621bf61" + integrity sha512-v9+8rT2gw/4zIRBwc4fIVhrTH/yFVDQgJgyYZjqr3fgojOypdQCOwkN6Z8dOwTei4/zo+b/zDPB4x1UvghJZRg== + dependencies: + "@docusaurus/types" "3.8.0" + "@rspack/core" "^1.3.10" + "@swc/core" "^1.7.39" + "@swc/html" "^1.7.39" + browserslist "^4.24.2" + lightningcss "^1.27.0" + swc-loader "^0.2.6" + tslib "^2.6.0" + webpack "^5.95.0" + +"@docusaurus/logger@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.8.0.tgz#c1abbb084a8058dc0047d57070fb9cd0241a679d" + integrity sha512-7eEMaFIam5Q+v8XwGqF/n0ZoCld4hV4eCCgQkfcN9Mq5inoZa6PHHW9Wu6lmgzoK5Kx3keEeABcO2SxwraoPDQ== dependencies: chalk "^4.1.2" tslib "^2.6.0" -"@docusaurus/mdx-loader@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.4.0.tgz#483d7ab57928fdbb5c8bd1678098721a930fc5f6" - integrity sha512-kSSbrrk4nTjf4d+wtBA9H+FGauf2gCax89kV8SUSJu3qaTdSIKdWERlngsiHaCFgZ7laTJ8a67UFf+xlFPtuTw== +"@docusaurus/mdx-loader@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.8.0.tgz#2b225cd2b1159cc49b10b1cac63a927a8368274b" + integrity sha512-mDPSzssRnpjSdCGuv7z2EIAnPS1MHuZGTaRLwPn4oQwszu4afjWZ/60sfKjTnjBjI8Vl4OgJl2vMmfmiNDX4Ng== dependencies: - "@docusaurus/logger" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" "@mdx-js/mdx" "^3.0.0" "@slorber/remark-comment" "^1.0.0" escape-html "^1.0.3" estree-util-value-to-estree "^3.0.1" file-loader "^6.2.0" fs-extra "^11.1.1" - image-size "^1.0.2" + image-size "^2.0.2" mdast-util-mdx "^3.0.0" mdast-util-to-string "^4.0.0" rehype-raw "^7.0.0" @@ -1469,176 +2711,206 @@ vfile "^6.0.1" webpack "^5.88.1" -"@docusaurus/module-type-aliases@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz#2653bde58fc1aa3dbc626a6c08cfb63a37ae1bb8" - integrity sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw== +"@docusaurus/module-type-aliases@3.8.0", "@docusaurus/module-type-aliases@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.8.0.tgz#e487052c372538c5dcf2200999e13f328fa5ffaa" + integrity sha512-/uMb4Ipt5J/QnD13MpnoC/A4EYAe6DKNWqTWLlGrqsPJwJv73vSwkA25xnYunwfqWk0FlUQfGv/Swdh5eCCg7g== dependencies: - "@docusaurus/types" "3.4.0" + "@docusaurus/types" "3.8.0" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" "@types/react-router-dom" "*" - react-helmet-async "*" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" react-loadable "npm:@docusaurus/react-loadable@6.0.0" -"@docusaurus/plugin-content-blog@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.4.0.tgz#6373632fdbababbda73a13c4a08f907d7de8f007" - integrity sha512-vv6ZAj78ibR5Jh7XBUT4ndIjmlAxkijM3Sx5MAAzC1gyv0vupDQNhzuFg1USQmQVj3P5I6bquk12etPV3LJ+Xw== +"@docusaurus/plugin-content-blog@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.8.0.tgz#0c200b1fb821e09e9e975c45255e5ddfab06c392" + integrity sha512-0SlOTd9R55WEr1GgIXu+hhTT0hzARYx3zIScA5IzpdekZQesI/hKEa5LPHBd415fLkWMjdD59TaW/3qQKpJ0Lg== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - cheerio "^1.0.0-rc.12" + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + cheerio "1.0.0-rc.12" feed "^4.2.2" fs-extra "^11.1.1" lodash "^4.17.21" - reading-time "^1.5.0" + schema-dts "^1.1.2" srcset "^4.0.0" tslib "^2.6.0" unist-util-visit "^5.0.0" utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-docs@3.4.0", "@docusaurus/plugin-content-docs@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.4.0.tgz#3088973f72169a2a6d533afccec7153c8720d332" - integrity sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg== +"@docusaurus/plugin-content-docs@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.0.tgz#6aedb1261da1f0c8c2fa11cfaa6df4577a9b7826" + integrity sha512-fRDMFLbUN6eVRXcjP8s3Y7HpAt9pzPYh1F/7KKXOCxvJhjjCtbon4VJW0WndEPInVz4t8QUXn5QZkU2tGVCE2g== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/module-type-aliases" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" "@types/react-router-config" "^5.0.7" combine-promises "^1.1.0" fs-extra "^11.1.1" js-yaml "^4.1.0" lodash "^4.17.21" + schema-dts "^1.1.2" tslib "^2.6.0" utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-pages@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.4.0.tgz#1846172ca0355c7d32a67ef8377750ce02bbb8ad" - integrity sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg== +"@docusaurus/plugin-content-pages@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.8.0.tgz#2db5f990872684c621665d0d0d8d9b5831fd2999" + integrity sha512-39EDx2y1GA0Pxfion5tQZLNJxL4gq6susd1xzetVBjVIQtwpCdyloOfQBAgX0FylqQxfJrYqL0DIUuq7rd7uBw== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" fs-extra "^11.1.1" tslib "^2.6.0" webpack "^5.88.1" -"@docusaurus/plugin-debug@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.4.0.tgz#74e4ec5686fa314c26f3ac150bacadbba7f06948" - integrity sha512-uV7FDUNXGyDSD3PwUaf5YijX91T5/H9SX4ErEcshzwgzWwBtK37nUWPU3ZLJfeTavX3fycTOqk9TglpOLaWkCg== +"@docusaurus/plugin-css-cascade-layers@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.8.0.tgz#a0741ae32917a88ce7ce76b6f472495fa4bf576d" + integrity sha512-/VBTNymPIxQB8oA3ZQ4GFFRYdH4ZxDRRBECxyjRyv486mfUPXfcdk+im4S5mKWa6EK2JzBz95IH/Wu0qQgJ5yQ== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + tslib "^2.6.0" + +"@docusaurus/plugin-debug@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.8.0.tgz#297c159ae99924e60042426d2ad6ee0d5e9126b3" + integrity sha512-teonJvJsDB9o2OnG6ifbhblg/PXzZvpUKHFgD8dOL1UJ58u0lk8o0ZOkvaYEBa9nDgqzoWrRk9w+e3qaG2mOhQ== + dependencies: + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" fs-extra "^11.1.1" - react-json-view-lite "^1.2.0" + react-json-view-lite "^2.3.0" tslib "^2.6.0" -"@docusaurus/plugin-google-analytics@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.4.0.tgz#5f59fc25329a59decc231936f6f9fb5663da3c55" - integrity sha512-mCArluxEGi3cmYHqsgpGGt3IyLCrFBxPsxNZ56Mpur0xSlInnIHoeLDH7FvVVcPJRPSQ9/MfRqLsainRw+BojA== +"@docusaurus/plugin-google-analytics@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.8.0.tgz#fb97097af331beb13553a384081dc83607539b31" + integrity sha512-aKKa7Q8+3xRSRESipNvlFgNp3FNPELKhuo48Cg/svQbGNwidSHbZT03JqbW4cBaQnyyVchO1ttk+kJ5VC9Gx0w== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" tslib "^2.6.0" -"@docusaurus/plugin-google-gtag@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.4.0.tgz#42489ac5fe1c83b5523ceedd5ef74f9aa8bc251b" - integrity sha512-Dsgg6PLAqzZw5wZ4QjUYc8Z2KqJqXxHxq3vIoyoBWiLEEfigIs7wHR+oiWUQy3Zk9MIk6JTYj7tMoQU0Jm3nqA== +"@docusaurus/plugin-google-gtag@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.8.0.tgz#b5a60006c28ac582859a469fb92e53d383b0a055" + integrity sha512-ugQYMGF4BjbAW/JIBtVcp+9eZEgT9HRdvdcDudl5rywNPBA0lct+lXMG3r17s02rrhInMpjMahN3Yc9Cb3H5/g== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" "@types/gtag.js" "^0.0.12" tslib "^2.6.0" -"@docusaurus/plugin-google-tag-manager@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.4.0.tgz#cebb03a5ffa1e70b37d95601442babea251329ff" - integrity sha512-O9tX1BTwxIhgXpOLpFDueYA9DWk69WCbDRrjYoMQtFHSkTyE7RhNgyjSPREUWJb9i+YUg3OrsvrBYRl64FCPCQ== +"@docusaurus/plugin-google-tag-manager@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.8.0.tgz#612aa63e161fb273bf7db2591034c0142951727d" + integrity sha512-9juRWxbwZD3SV02Jd9QB6yeN7eu+7T4zB0bvJLcVQwi+am51wAxn2CwbdL0YCCX+9OfiXbADE8D8Q65Hbopu/w== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" tslib "^2.6.0" -"@docusaurus/plugin-sitemap@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.4.0.tgz#b091d64d1e3c6c872050189999580187537bcbc6" - integrity sha512-+0VDvx9SmNrFNgwPoeoCha+tRoAjopwT0+pYO1xAbyLcewXSemq+eLxEa46Q1/aoOaJQ0qqHELuQM7iS2gp33Q== +"@docusaurus/plugin-sitemap@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.8.0.tgz#a39e3b5aa2f059aba0052ed11a6b4fbf78ac0dad" + integrity sha512-fGpOIyJvNiuAb90nSJ2Gfy/hUOaDu6826e5w5UxPmbpCIc7KlBHNAZ5g4L4ZuHhc4hdfq4mzVBsQSnne+8Ze1g== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" fs-extra "^11.1.1" sitemap "^7.1.1" tslib "^2.6.0" -"@docusaurus/preset-classic@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.4.0.tgz#6082a32fbb465b0cb2c2a50ebfc277cff2c0f139" - integrity sha512-Ohj6KB7siKqZaQhNJVMBBUzT3Nnp6eTKqO+FXO3qu/n1hJl3YLwVKTWBg28LF7MWrKu46UuYavwMRxud0VyqHg== +"@docusaurus/plugin-svgr@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-svgr/-/plugin-svgr-3.8.0.tgz#6d2d43f14b32b4bb2dd8dc87a70c6e78754c1e85" + integrity sha512-kEDyry+4OMz6BWLG/lEqrNsL/w818bywK70N1gytViw4m9iAmoxCUT7Ri9Dgs7xUdzCHJ3OujolEmD88Wy44OA== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/plugin-content-blog" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/plugin-content-pages" "3.4.0" - "@docusaurus/plugin-debug" "3.4.0" - "@docusaurus/plugin-google-analytics" "3.4.0" - "@docusaurus/plugin-google-gtag" "3.4.0" - "@docusaurus/plugin-google-tag-manager" "3.4.0" - "@docusaurus/plugin-sitemap" "3.4.0" - "@docusaurus/theme-classic" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/theme-search-algolia" "3.4.0" - "@docusaurus/types" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + "@svgr/core" "8.1.0" + "@svgr/webpack" "^8.1.0" + tslib "^2.6.0" + webpack "^5.88.1" -"@docusaurus/theme-classic@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.4.0.tgz#1b0f48edec3e3ec8927843554b9f11e5927b0e52" - integrity sha512-0IPtmxsBYv2adr1GnZRdMkEQt1YW6tpzrUPj02YxNpvJ5+ju4E13J5tB4nfdaen/tfR1hmpSPlTFPvTf4kwy8Q== +"@docusaurus/preset-classic@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.8.0.tgz#ac8bc17e3b7b443d8a24f2f1da0c0be396950fef" + integrity sha512-qOu6tQDOWv+rpTlKu+eJATCJVGnABpRCPuqf7LbEaQ1mNY//N/P8cHQwkpAU+aweQfarcZ0XfwCqRHJfjeSV/g== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/plugin-content-blog" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/plugin-content-pages" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/theme-translations" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/plugin-content-blog" "3.8.0" + "@docusaurus/plugin-content-docs" "3.8.0" + "@docusaurus/plugin-content-pages" "3.8.0" + "@docusaurus/plugin-css-cascade-layers" "3.8.0" + "@docusaurus/plugin-debug" "3.8.0" + "@docusaurus/plugin-google-analytics" "3.8.0" + "@docusaurus/plugin-google-gtag" "3.8.0" + "@docusaurus/plugin-google-tag-manager" "3.8.0" + "@docusaurus/plugin-sitemap" "3.8.0" + "@docusaurus/plugin-svgr" "3.8.0" + "@docusaurus/theme-classic" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/theme-search-algolia" "3.8.0" + "@docusaurus/types" "3.8.0" + +"@docusaurus/theme-classic@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.8.0.tgz#6d44fb801b86a7c7af01cda0325af1a3300b3ac2" + integrity sha512-nQWFiD5ZjoT76OaELt2n33P3WVuuCz8Dt5KFRP2fCBo2r9JCLsp2GJjZpnaG24LZ5/arRjv4VqWKgpK0/YLt7g== + dependencies: + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/module-type-aliases" "3.8.0" + "@docusaurus/plugin-content-blog" "3.8.0" + "@docusaurus/plugin-content-docs" "3.8.0" + "@docusaurus/plugin-content-pages" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/theme-translations" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" "@mdx-js/react" "^3.0.0" clsx "^2.0.0" copy-text-to-clipboard "^3.2.0" - infima "0.2.0-alpha.43" + infima "0.2.0-alpha.45" lodash "^4.17.21" nprogress "^0.2.0" postcss "^8.4.26" @@ -1649,18 +2921,15 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-common@3.4.0", "@docusaurus/theme-common@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.4.0.tgz#01f2b728de6cb57f6443f52fc30675cf12a5d49f" - integrity sha512-0A27alXuv7ZdCg28oPE8nH/Iz73/IUejVaCazqu9elS4ypjiLhK3KfzdSQBnL/g7YfHSlymZKdiOHEo8fJ0qMA== +"@docusaurus/theme-common@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.8.0.tgz#102c385c3d1d3b7a6b52d1911c7e88c38d9a977e" + integrity sha512-YqV2vAWpXGLA+A3PMLrOMtqgTHJLDcT+1Caa6RF7N4/IWgrevy5diY8oIHFkXR/eybjcrFFjUPrHif8gSGs3Tw== dependencies: - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/plugin-content-blog" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/plugin-content-pages" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/module-type-aliases" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" @@ -1670,34 +2939,34 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-mermaid@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.4.0.tgz#ef1d2231d0858767f67538b4fafd7d0ce2a3e845" - integrity sha512-3w5QW0HEZ2O6x2w6lU3ZvOe1gNXP2HIoKDMJBil1VmLBc9PmpAG17VmfhI/p3L2etNmOiVs5GgniUqvn8AFEGQ== +"@docusaurus/theme-mermaid@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.8.0.tgz#f0720ec89fd386870f30978ba984b1b126ca92a5" + integrity sha512-ou0NJM37p4xrVuFaZp8qFe5Z/qBq9LuyRTP4KKRa0u2J3zC4f3saBJDgc56FyvvN1OsmU0189KGEPUjTr6hFxg== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - mermaid "^10.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/module-type-aliases" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + mermaid ">=11.6.0" tslib "^2.6.0" -"@docusaurus/theme-search-algolia@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz#c499bad71d668df0d0f15b0e5e33e2fc4e330fcc" - integrity sha512-aiHFx7OCw4Wck1z6IoShVdUWIjntC8FHCw9c5dR8r3q4Ynh+zkS8y2eFFunN/DL6RXPzpnvKCg3vhLQYJDmT9Q== +"@docusaurus/theme-search-algolia@3.8.0", "@docusaurus/theme-search-algolia@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.8.0.tgz#21c2f18e07a73d13ca3b44fcf0ae9aac33bef60f" + integrity sha512-GBZ5UOcPgiu6nUw153+0+PNWvFKweSnvKIL6Rp04H9olKb475jfKjAwCCtju5D2xs5qXHvCMvzWOg5o9f6DtuQ== dependencies: - "@docsearch/react" "^3.5.2" - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/theme-translations" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - algoliasearch "^4.18.0" - algoliasearch-helper "^3.13.3" + "@docsearch/react" "^3.9.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/plugin-content-docs" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/theme-translations" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + algoliasearch "^5.17.1" + algoliasearch-helper "^3.22.6" clsx "^2.0.0" eta "^2.2.0" fs-extra "^11.1.1" @@ -1705,15 +2974,30 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-translations@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.4.0.tgz#e6355d01352886c67e38e848b2542582ea3070af" - integrity sha512-zSxCSpmQCCdQU5Q4CnX/ID8CSUUI3fvmq4hU/GNP/XoAWtXo9SAVnM3TzpU8Gb//H3WCsT8mJcTfyOk3d9ftNg== +"@docusaurus/theme-translations@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.8.0.tgz#deb64dccab74361624c3cb352a4949a7ac868c74" + integrity sha512-1DTy/snHicgkCkryWq54fZvsAglTdjTx4qjOXgqnXJ+DIty1B+aPQrAVUu8LiM+6BiILfmNxYsxhKTj+BS3PZg== dependencies: fs-extra "^11.1.1" tslib "^2.6.0" -"@docusaurus/types@3.4.0", "@docusaurus/types@^3.0.0": +"@docusaurus/types@3.8.0", "@docusaurus/types@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.8.0.tgz#f6cd31c4e3e392e0270b8137d7fe4365ea7a022e" + integrity sha512-RDEClpwNxZq02c+JlaKLWoS13qwWhjcNsi2wG1UpzmEnuti/z1Wx4SGpqbUqRPNSd8QWWePR8Cb7DvG0VN/TtA== + dependencies: + "@mdx-js/mdx" "^3.0.0" + "@types/history" "^4.7.11" + "@types/react" "*" + commander "^5.1.0" + joi "^17.9.2" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" + utility-types "^3.10.0" + webpack "^5.95.0" + webpack-merge "^5.9.0" + +"@docusaurus/types@^3.0.0": version "3.4.0" resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.4.0.tgz#237c3f737e9db3f7c1a5935a3ef48d6eadde8292" integrity sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A== @@ -1728,36 +3012,38 @@ webpack "^5.88.1" webpack-merge "^5.9.0" -"@docusaurus/utils-common@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.4.0.tgz#2a43fefd35b85ab9fcc6833187e66c15f8bfbbc6" - integrity sha512-NVx54Wr4rCEKsjOH5QEVvxIqVvm+9kh7q8aYTU5WzUU9/Hctd6aTrcZ3G0Id4zYJ+AeaG5K5qHA4CY5Kcm2iyQ== +"@docusaurus/utils-common@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.8.0.tgz#2b1a6b1ec4a7fac62f1898d523d42f8cc4a8258f" + integrity sha512-3TGF+wVTGgQ3pAc9+5jVchES4uXUAhAt9pwv7uws4mVOxL4alvU3ue/EZ+R4XuGk94pDy7CNXjRXpPjlfZXQfw== dependencies: + "@docusaurus/types" "3.8.0" tslib "^2.6.0" -"@docusaurus/utils-validation@3.4.0", "@docusaurus/utils-validation@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.4.0.tgz#0176f6e503ff45f4390ec2ecb69550f55e0b5eb7" - integrity sha512-hYQ9fM+AXYVTWxJOT1EuNaRnrR2WGpRdLDQG07O8UOpsvCPWUVOeo26Rbm0JWY2sGLfzAb+tvJ62yF+8F+TV0g== +"@docusaurus/utils-validation@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.8.0.tgz#aa02e9d998e20998fbcaacd94873878bc3b9a4cd" + integrity sha512-MrnEbkigr54HkdFeg8e4FKc4EF+E9dlVwsY3XQZsNkbv3MKZnbHQ5LsNJDIKDROFe8PBf5C4qCAg5TPBpsjrjg== dependencies: - "@docusaurus/logger" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" fs-extra "^11.2.0" joi "^17.9.2" js-yaml "^4.1.0" lodash "^4.17.21" tslib "^2.6.0" -"@docusaurus/utils@3.4.0", "@docusaurus/utils@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.4.0.tgz#c508e20627b7a55e2b541e4a28c95e0637d6a204" - integrity sha512-fRwnu3L3nnWaXOgs88BVBmG1yGjcQqZNHG+vInhEa2Sz2oQB+ZjbEMO5Rh9ePFpZ0YDiDUhpaVjwmS+AU2F14g== +"@docusaurus/utils@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.8.0.tgz#92bad89d2a11f5f246196af153093b12cd79f9ac" + integrity sha512-2wvtG28ALCN/A1WCSLxPASFBFzXCnP0YKCAFIPcvEb6imNu1wg7ni/Svcp71b3Z2FaOFFIv4Hq+j4gD7gA0yfQ== dependencies: - "@docusaurus/logger" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@svgr/webpack" "^8.1.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-common" "3.8.0" escape-string-regexp "^4.0.0" + execa "5.1.1" file-loader "^6.2.0" fs-extra "^11.1.1" github-slugger "^1.5.0" @@ -1767,9 +3053,9 @@ js-yaml "^4.1.0" lodash "^4.17.21" micromatch "^4.0.5" + p-queue "^6.6.2" prompts "^2.4.2" resolve-pathname "^3.0.0" - shelljs "^0.8.5" tslib "^2.6.0" url-loader "^4.1.1" utility-types "^3.10.0" @@ -1815,6 +3101,25 @@ resolved "https://registry.yarnpkg.com/@hookform/error-message/-/error-message-2.0.1.tgz#6a37419106e13664ad6a29c9dae699ae6cd276b8" integrity sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg== +"@iconify/types@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57" + integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg== + +"@iconify/utils@^2.1.33": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@iconify/utils/-/utils-2.3.0.tgz#1bbbf8c477ebe9a7cacaea78b1b7e8937f9cbfba" + integrity sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA== + dependencies: + "@antfu/install-pkg" "^1.0.0" + "@antfu/utils" "^8.1.0" + "@iconify/types" "^2.0.0" + debug "^4.4.0" + globals "^15.14.0" + kolorist "^1.8.0" + local-pkg "^1.0.0" + mlly "^1.7.4" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -1932,6 +3237,56 @@ dependencies: "@types/mdx" "^2.0.0" +"@mermaid-js/parser@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@mermaid-js/parser/-/parser-0.4.0.tgz#c1de1f5669f8fcbd0d0c9d124927d36ddc00d8a6" + integrity sha512-wla8XOWvQAwuqy+gxiZqY+c7FokraOTHRWMsbB4AgRx9Sy7zKslNyejy7E+a77qHfey5GXw/ik3IXv/NHMJgaA== + dependencies: + langium "3.3.1" + +"@module-federation/error-codes@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/error-codes/-/error-codes-0.14.0.tgz#d54581bfb998ce9ace4cb33f8795c644e461bfeb" + integrity sha512-GGk+EoeSACJikZZyShnLshtq9E2eCrDWbRiB4QAFXCX4oYmGgFfzXlx59vMNwqTKPJWxkEGnPYacJMcr2YYjag== + +"@module-federation/runtime-core@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/runtime-core/-/runtime-core-0.14.0.tgz#a00a3666cc25a8bb822a36552c631e6e3f7326cc" + integrity sha512-fGE1Ro55zIFDp/CxQuRhKQ1pJvG7P0qvRm2N+4i8z++2bgDjcxnCKUqDJ8lLD+JfJQvUJf0tuSsJPgevzueD4g== + dependencies: + "@module-federation/error-codes" "0.14.0" + "@module-federation/sdk" "0.14.0" + +"@module-federation/runtime-tools@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/runtime-tools/-/runtime-tools-0.14.0.tgz#f5b5f3d19605b6d7c90ed278dc00a5400f1aa49d" + integrity sha512-y/YN0c2DKsLETE+4EEbmYWjqF9G6ZwgZoDIPkaQ9p0pQu0V4YxzWfQagFFxR0RigYGuhJKmSU/rtNoHq+qF8jg== + dependencies: + "@module-federation/runtime" "0.14.0" + "@module-federation/webpack-bundler-runtime" "0.14.0" + +"@module-federation/runtime@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/runtime/-/runtime-0.14.0.tgz#e04012d2d928275fd00525904c0b4f8be514dc70" + integrity sha512-kR3cyHw/Y64SEa7mh4CHXOEQYY32LKLK75kJOmBroLNLO7/W01hMNAvGBYTedS7hWpVuefPk1aFZioy3q2VLdQ== + dependencies: + "@module-federation/error-codes" "0.14.0" + "@module-federation/runtime-core" "0.14.0" + "@module-federation/sdk" "0.14.0" + +"@module-federation/sdk@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/sdk/-/sdk-0.14.0.tgz#efa38341b7601f58967397cc630068f69691b931" + integrity sha512-lg/OWRsh18hsyTCamOOhEX546vbDiA2O4OggTxxH2wTGr156N6DdELGQlYIKfRdU/0StgtQS81Goc0BgDZlx9A== + +"@module-federation/webpack-bundler-runtime@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.14.0.tgz#21a82505f95fdb3cb202786f8dc20611b4d7f93c" + integrity sha512-POWS6cKBicAAQ3DNY5X7XEUSfOfUsRaBNxbuwEfSGlrkTE9UcWheO06QP2ndHi8tHQuUKcIHi2navhPkJ+k5xg== + dependencies: + "@module-federation/runtime" "0.14.0" + "@module-federation/sdk" "0.14.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1953,6 +3308,95 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@parcel/watcher-android-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" + integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA== + +"@parcel/watcher-darwin-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67" + integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw== + +"@parcel/watcher-darwin-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8" + integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg== + +"@parcel/watcher-freebsd-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b" + integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ== + +"@parcel/watcher-linux-arm-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1" + integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA== + +"@parcel/watcher-linux-arm-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e" + integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q== + +"@parcel/watcher-linux-arm64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30" + integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w== + +"@parcel/watcher-linux-arm64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2" + integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg== + +"@parcel/watcher-linux-x64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e" + integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A== + +"@parcel/watcher-linux-x64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee" + integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg== + +"@parcel/watcher-win32-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243" + integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw== + +"@parcel/watcher-win32-ia32@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6" + integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ== + +"@parcel/watcher-win32-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947" + integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA== + +"@parcel/watcher@^2.4.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.1.tgz#342507a9cfaaf172479a882309def1e991fb1200" + integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.1" + "@parcel/watcher-darwin-arm64" "2.5.1" + "@parcel/watcher-darwin-x64" "2.5.1" + "@parcel/watcher-freebsd-x64" "2.5.1" + "@parcel/watcher-linux-arm-glibc" "2.5.1" + "@parcel/watcher-linux-arm-musl" "2.5.1" + "@parcel/watcher-linux-arm64-glibc" "2.5.1" + "@parcel/watcher-linux-arm64-musl" "2.5.1" + "@parcel/watcher-linux-x64-glibc" "2.5.1" + "@parcel/watcher-linux-x64-musl" "2.5.1" + "@parcel/watcher-win32-arm64" "2.5.1" + "@parcel/watcher-win32-ia32" "2.5.1" + "@parcel/watcher-win32-x64" "2.5.1" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -2025,6 +3469,81 @@ redux-thunk "^2.4.2" reselect "^4.1.8" +"@rspack/binding-darwin-arm64@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.3.12.tgz#2e7cc00b813dcb155572908d956ab1d75d9747a5" + integrity sha512-8hKjVTBeWPqkMzFPNWIh72oU9O3vFy3e88wRjMPImDCXBiEYrKqGTTLd/J0SO+efdL3SBD1rX1IvdJpxCv6Yrw== + +"@rspack/binding-darwin-x64@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.3.12.tgz#cb148cc62658d74204621a695e1698cda82877f9" + integrity sha512-Sj4m+mCUxL7oCpdu7OmWT7fpBM7hywk5CM9RDc3D7StaBZbvNtNftafCrTZzTYKuZrKmemTh5SFzT5Tz7tf6GA== + +"@rspack/binding-linux-arm64-gnu@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.3.12.tgz#79820857dfbd3819e0026da507c271531c013d0c" + integrity sha512-7MuOxf3/Mhv4mgFdLTvgnt/J+VouNR65DEhorth+RZm3LEWojgoFEphSAMAvpvAOpYSS68Sw4SqsOZi719ia2w== + +"@rspack/binding-linux-arm64-musl@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.3.12.tgz#a794dfe8df6bf75af217597881592add6f6b046e" + integrity sha512-s6KKj20T9Z1bA8caIjU6EzJbwyDo1URNFgBAlafCT2UC6yX7flstDJJ38CxZacA9A2P24RuQK2/jPSZpWrTUFA== + +"@rspack/binding-linux-x64-gnu@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.3.12.tgz#a0e23831a1374d9039a4b29e927cef58a485085c" + integrity sha512-0w/sRREYbRgHgWvs2uMEJSLfvzbZkPHUg6CMcYQGNVK6axYRot6jPyKetyFYA9pR5fB5rsXegpnFaZaVrRIK2g== + +"@rspack/binding-linux-x64-musl@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.3.12.tgz#61620efda6a6805689890c130e6b38c1725baaf4" + integrity sha512-jEdxkPymkRxbijDRsBGdhopcbGXiXDg59lXqIRkVklqbDmZ/O6DHm7gImmlx5q9FoWbz0gqJuOKBz4JqWxjWVA== + +"@rspack/binding-win32-arm64-msvc@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.3.12.tgz#46d874df8bd5b84e82ea83480969f7e0293ccd82" + integrity sha512-ZRvUCb3TDLClAqcTsl/o9UdJf0B5CgzAxgdbnYJbldyuyMeTUB4jp20OfG55M3C2Nute2SNhu2bOOp9Se5Ongw== + +"@rspack/binding-win32-ia32-msvc@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.3.12.tgz#fec435e31e56f3d58b4fa52746643d1d593b2b89" + integrity sha512-1TKPjuXStPJr14f3ZHuv40Xc/87jUXx10pzVtrPnw+f3hckECHrbYU/fvbVzZyuXbsXtkXpYca6ygCDRJAoNeQ== + +"@rspack/binding-win32-x64-msvc@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.3.12.tgz#33b73cbab75920cf8a92e7245a794970f8508b6c" + integrity sha512-lCR0JfnYKpV+a6r2A2FdxyUKUS4tajePgpPJN5uXDgMGwrDtRqvx+d0BHhwjFudQVJq9VVbRaL89s2MQ6u+xYw== + +"@rspack/binding@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding/-/binding-1.3.12.tgz#0a8356fdbd89f08cda3e9bb8aff4ea781dfe972e" + integrity sha512-4Ic8lV0+LCBfTlH5aIOujIRWZOtgmG223zC4L3o8WY/+ESAgpdnK6lSSMfcYgRanYLAy3HOmFIp20jwskMpbAg== + optionalDependencies: + "@rspack/binding-darwin-arm64" "1.3.12" + "@rspack/binding-darwin-x64" "1.3.12" + "@rspack/binding-linux-arm64-gnu" "1.3.12" + "@rspack/binding-linux-arm64-musl" "1.3.12" + "@rspack/binding-linux-x64-gnu" "1.3.12" + "@rspack/binding-linux-x64-musl" "1.3.12" + "@rspack/binding-win32-arm64-msvc" "1.3.12" + "@rspack/binding-win32-ia32-msvc" "1.3.12" + "@rspack/binding-win32-x64-msvc" "1.3.12" + +"@rspack/core@^1.3.10": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/core/-/core-1.3.12.tgz#68df0111cfac7e8f9dfa11a608ac8731181b5483" + integrity sha512-mAPmV4LPPRgxpouUrGmAE4kpF1NEWJGyM5coebsjK/zaCMSjw3mkdxiU2b5cO44oIi0Ifv5iGkvwbdrZOvMyFA== + dependencies: + "@module-federation/runtime-tools" "0.14.0" + "@rspack/binding" "1.3.12" + "@rspack/lite-tapable" "1.0.1" + caniuse-lite "^1.0.30001718" + +"@rspack/lite-tapable@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz#d4540a5d28bd6177164bc0ba0bee4bdec0458591" + integrity sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w== + "@sideway/address@^4.1.5": version "4.1.5" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" @@ -2172,84 +3691,152 @@ "@svgr/plugin-jsx" "8.1.0" "@svgr/plugin-svgo" "8.1.0" -"@swc/core-darwin-arm64@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.1.tgz#72d861fb7094b7a0004f4f300e2c5d4ea1549d9e" - integrity sha512-u6GdwOXsOEdNAdSI6nWq6G2BQw5HiSNIZVcBaH1iSvBnxZvWbnIKyDiZKaYnDwTLHLzig2GuUjjE2NaCJPy4jg== +"@swc/core-darwin-arm64@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.29.tgz#bf66e3f4f00e6fe9d95e8a33f780e6c40fca946d" + integrity sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ== -"@swc/core-darwin-x64@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.6.1.tgz#8b7070fcee4a4570d0af245c4614ca4e492dfd5b" - integrity sha512-/tXwQibkDNLVbAtr7PUQI0iQjoB708fjhDDDfJ6WILSBVZ3+qs/LHjJ7jHwumEYxVq1XA7Fv2Q7SE/ZSQoWHcQ== +"@swc/core-darwin-x64@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.11.29.tgz#0a77d2d79ef2c789f9d40a86784bbf52c5f9877f" + integrity sha512-S3eTo/KYFk+76cWJRgX30hylN5XkSmjYtCBnM4jPLYn7L6zWYEPajsFLmruQEiTEDUg0gBEWLMNyUeghtswouw== -"@swc/core-linux-arm-gnueabihf@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.1.tgz#bea6d2e75127bbc65a664284f012ffa90c8325d5" - integrity sha512-aDgipxhJTms8iH78emHVutFR2c16LNhO+NTRCdYi+X4PyIn58/DyYTH6VDZ0AeEcS5f132ZFldU5AEgExwihXA== +"@swc/core-linux-arm-gnueabihf@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.29.tgz#80fa3a6a36034ffdbbba73e26c8f27cb13111a33" + integrity sha512-o9gdshbzkUMG6azldHdmKklcfrcMx+a23d/2qHQHPDLUPAN+Trd+sDQUYArK5Fcm7TlpG4sczz95ghN0DMkM7g== -"@swc/core-linux-arm64-gnu@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.1.tgz#5c84d804ec23cf54b31c0bc0b4bdd30ec5d43ce8" - integrity sha512-XkJ+eO4zUKG5g458RyhmKPyBGxI0FwfWFgpfIj5eDybxYJ6s4HBT5MoxyBLorB5kMlZ0XoY/usUMobPVY3nL0g== +"@swc/core-linux-arm64-gnu@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.29.tgz#42da87f445bc3e26da01d494246884006d9b9a1a" + integrity sha512-sLoaciOgUKQF1KX9T6hPGzvhOQaJn+3DHy4LOHeXhQqvBgr+7QcZ+hl4uixPKTzxk6hy6Hb0QOvQEdBAAR1gXw== -"@swc/core-linux-arm64-musl@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.1.tgz#e167a350bec12caebc97304068c3ffbad6c398ce" - integrity sha512-dr6YbLBg/SsNxs1hDqJhxdcrS8dGMlOXJwXIrUvACiA8jAd6S5BxYCaqsCefLYXtaOmu0bbx1FB/evfodqB70Q== +"@swc/core-linux-arm64-musl@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.29.tgz#c9cec610525dc9e9b11ef26319db3780812dfa54" + integrity sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw== -"@swc/core-linux-x64-gnu@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.1.tgz#fdd4e1d63b3e53d195e2ddcb9cb5ad9f31995796" - integrity sha512-A0b/3V+yFy4LXh3O9umIE7LXPC7NBWdjl6AQYqymSMcMu0EOb1/iygA6s6uWhz9y3e172Hpb9b/CGsuD8Px/bg== +"@swc/core-linux-x64-gnu@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.29.tgz#1cda2df38a4ab8905ba6ac3aa16e4ad710b6f2de" + integrity sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA== -"@swc/core-linux-x64-musl@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.1.tgz#81a312dd9e62da5f4c48e3cd23b6c6d28a31ac42" - integrity sha512-5dJjlzZXhC87nZZZWbpiDP8kBIO0ibis893F/rtPIQBI5poH+iJuA32EU3wN4/WFHeK4et8z6SGSVghPtWyk4g== +"@swc/core-linux-x64-musl@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.29.tgz#5d634efff33f47c8d6addd84291ab606903d1cfd" + integrity sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ== -"@swc/core-win32-arm64-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.1.tgz#e131f579a69c5d807013e54ccb311e10caa27bcb" - integrity sha512-HBi1ZlwvfcUibLtT3g/lP57FaDPC799AD6InolB2KSgkqyBbZJ9wAXM8/CcH67GLIP0tZ7FqblrJTzGXxetTJQ== +"@swc/core-win32-arm64-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.29.tgz#bc54f2e3f8f180113b7a092b1ee1eaaab24df62b" + integrity sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw== -"@swc/core-win32-ia32-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.1.tgz#9f3d88cf0e826aa8222a695177a065ed2899eb21" - integrity sha512-AKqHohlWERclexar5y6ux4sQ8yaMejEXNxeKXm7xPhXrp13/1p4/I3E5bPVX/jMnvpm4HpcKSP0ee2WsqmhhPw== +"@swc/core-win32-ia32-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.29.tgz#f1df344c06283643d1fe66c6931b350347b73722" + integrity sha512-h+NjOrbqdRBYr5ItmStmQt6x3tnhqgwbj9YxdGPepbTDamFv7vFnhZR0YfB3jz3UKJ8H3uGJ65Zw1VsC+xpFkg== -"@swc/core-win32-x64-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.1.tgz#b2082710bc46c484a2c9f2e33a15973806e5031d" - integrity sha512-0dLdTLd+ONve8kgC5T6VQ2Y5G+OZ7y0ujjapnK66wpvCBM6BKYGdT/OKhZKZydrC5gUKaxFN6Y5oOt9JOFUrOQ== +"@swc/core-win32-x64-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.29.tgz#a6f9dc1df66c8db96d70091abedd78cc52544724" + integrity sha512-Q8cs2BDV9wqDvqobkXOYdC+pLUSEpX/KvI0Dgfun1F+LzuLotRFuDhrvkU9ETJA6OnD2+Fn/ieHgloiKA/Mn/g== -"@swc/core@^1.3.74": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.6.1.tgz#a899a205cfaa8e23f805451ef4787987e03b8920" - integrity sha512-Yz5uj5hNZpS5brLtBvKY0L4s2tBAbQ4TjmW8xF1EC3YLFxQRrUjMP49Zm1kp/KYyYvTkSaG48Ffj2YWLu9nChw== +"@swc/core@^1.7.39": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.11.29.tgz#bce20113c47fcd6251d06262b8b8c063f8e86a20" + integrity sha512-g4mThMIpWbNhV8G2rWp5a5/Igv8/2UFRJx2yImrLGMgrDDYZIopqZ/z0jZxDgqNA1QDx93rpwNF7jGsxVWcMlA== dependencies: "@swc/counter" "^0.1.3" - "@swc/types" "^0.1.8" + "@swc/types" "^0.1.21" optionalDependencies: - "@swc/core-darwin-arm64" "1.6.1" - "@swc/core-darwin-x64" "1.6.1" - "@swc/core-linux-arm-gnueabihf" "1.6.1" - "@swc/core-linux-arm64-gnu" "1.6.1" - "@swc/core-linux-arm64-musl" "1.6.1" - "@swc/core-linux-x64-gnu" "1.6.1" - "@swc/core-linux-x64-musl" "1.6.1" - "@swc/core-win32-arm64-msvc" "1.6.1" - "@swc/core-win32-ia32-msvc" "1.6.1" - "@swc/core-win32-x64-msvc" "1.6.1" + "@swc/core-darwin-arm64" "1.11.29" + "@swc/core-darwin-x64" "1.11.29" + "@swc/core-linux-arm-gnueabihf" "1.11.29" + "@swc/core-linux-arm64-gnu" "1.11.29" + "@swc/core-linux-arm64-musl" "1.11.29" + "@swc/core-linux-x64-gnu" "1.11.29" + "@swc/core-linux-x64-musl" "1.11.29" + "@swc/core-win32-arm64-msvc" "1.11.29" + "@swc/core-win32-ia32-msvc" "1.11.29" + "@swc/core-win32-x64-msvc" "1.11.29" "@swc/counter@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/types@^0.1.8": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.8.tgz#2c81d107c86cfbd0c3a05ecf7bb54c50dfa58a95" - integrity sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA== +"@swc/html-darwin-arm64@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-darwin-arm64/-/html-darwin-arm64-1.11.29.tgz#7bd6d10115ffe155ecd757387b5aff318b02b5a0" + integrity sha512-q53kn/HI0n/+pecsOB2gxqITbRAhtBG7VI520SIWuCGXHPsTQ/1VOrhLMNvyfw1xVhRyFal7BpAvfGUORCl0sw== + +"@swc/html-darwin-x64@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-darwin-x64/-/html-darwin-x64-1.11.29.tgz#ccafed56081932ffaa51be1788cef88eb9a144d1" + integrity sha512-YfQPjh5WoDqOxsA7vDOOSnxEPc1Ki4SuZ0ufR4t8jYdMOFsU3AhZQ/sgBZLpTzegBTutUn7/7yy8VSoFngeR7Q== + +"@swc/html-linux-arm-gnueabihf@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm-gnueabihf/-/html-linux-arm-gnueabihf-1.11.29.tgz#e784a1a0f69034e9dd52a9019ff80f0d5eb91433" + integrity sha512-dC3aEv1mqAUkY9TiZWOE2IcYpvxJzw0LdvkDzGW5072JSlZZYQMqq2Llwg63LIp6qBlj1JLHMLnBqk7Ubatmjw== + +"@swc/html-linux-arm64-gnu@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.11.29.tgz#ef015d81d3a011d6c273a428eee130b3aac790b7" + integrity sha512-seo+lCiBUggTR9NsHE4qVC+7+XIfLHK7yxWiIsXb8nNAXDcqVZ0Rxv8O1Y1GTeJfUlcCt1koahCG2AeyWpYFBg== + +"@swc/html-linux-arm64-musl@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-musl/-/html-linux-arm64-musl-1.11.29.tgz#b20e4b442287367c4c1d62db2b8065106542f432" + integrity sha512-bK8K6t3hHgaZZ1vMNaZ+8x42EWJPEX1Dx4zi6ulMhKa1uan+DjW5SiMlUg0an16fFSYfE+r9oFC4cFEbGP1o4Q== + +"@swc/html-linux-x64-gnu@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.11.29.tgz#c45505b3e22c02dc8bdef8b3da48ba855527a62c" + integrity sha512-34tSms5TkRUCr+J6uuSE/11ECcfIpp5R1ODuIgxZRUd/u88pQGKzLVNLWGPLw4b3cZSjnAn+PFJl7BtaYl0UyQ== + +"@swc/html-linux-x64-musl@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-musl/-/html-linux-x64-musl-1.11.29.tgz#86116e7db3cf02b1a627bc94c374d26ce6f9d68a" + integrity sha512-oJLLrX94ccaniWdQt8PH6K2u8aN/ehBo/YPg84LycFtaud/k73Fa1kh6Neq8vbWI4CugIWTl4LXWoHm+l+QYeA== + +"@swc/html-win32-arm64-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-win32-arm64-msvc/-/html-win32-arm64-msvc-1.11.29.tgz#cf7e57b8c0b52f7f93abc307b0cb78d8213b3c13" + integrity sha512-nw4TCFfA4YV6jicRdicJZPKW+ihOZPMKEG/4bj1/6HqXw1T2pXI070ASOLE0KOHYuoyV/jConEHfIjlU0olneA== + +"@swc/html-win32-ia32-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-win32-ia32-msvc/-/html-win32-ia32-msvc-1.11.29.tgz#61c91409c3fcdf942891c02aa2a2eac1892d1907" + integrity sha512-rO6X4qOofGpKV8pyZ7VblJn+J3PHEqeWHJkJfzwP7c04Flr1oLyuLbTU8lwf8enXrTAZqitHZs+OpofKcUwHEw== + +"@swc/html-win32-x64-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-win32-x64-msvc/-/html-win32-x64-msvc-1.11.29.tgz#0e78b507d2bf28315487655852008cdecfe84535" + integrity sha512-GSCihzBItEPJAeLzkAtw0ZGbxRGMsGt1Z1ugo0uHva1R3Eybkqu9qoax1tGAON+EJzeiHRqphhNgh8MVDpnKnQ== + +"@swc/html@^1.7.39": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html/-/html-1.11.29.tgz#6e9e1b8ea65baa0d6f25cb883565a5e7d22d2858" + integrity sha512-Tsk/o6Eo3lDvHPGjLqVwXGEdC1bemGzByPWx/TrF5N7qEsanRblPeRcJzLl6LbWa80pRYIRB6T4VqdXXZqklaw== + dependencies: + "@swc/counter" "^0.1.3" + optionalDependencies: + "@swc/html-darwin-arm64" "1.11.29" + "@swc/html-darwin-x64" "1.11.29" + "@swc/html-linux-arm-gnueabihf" "1.11.29" + "@swc/html-linux-arm64-gnu" "1.11.29" + "@swc/html-linux-arm64-musl" "1.11.29" + "@swc/html-linux-x64-gnu" "1.11.29" + "@swc/html-linux-x64-musl" "1.11.29" + "@swc/html-win32-arm64-msvc" "1.11.29" + "@swc/html-win32-ia32-msvc" "1.11.29" + "@swc/html-win32-x64-msvc" "1.11.29" + +"@swc/types@^0.1.21": + version "0.1.21" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.21.tgz#6fcadbeca1d8bc89e1ab3de4948cef12344a38c0" + integrity sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ== dependencies: "@swc/counter" "^0.1.3" @@ -2314,23 +3901,216 @@ dependencies: "@types/node" "*" -"@types/d3-scale-chromatic@^3.0.0": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz#fc0db9c10e789c351f4c42d96f31f2e4df8f5644" - integrity sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw== +"@types/d3-array@*": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== -"@types/d3-scale@^4.0.3": - version "4.0.8" - resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" - integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== +"@types/d3-axis@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795" + integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c" + integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-chord@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d" + integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-contour@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231" + integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-delaunay@*": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" + integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== + +"@types/d3-dispatch@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz#096efdf55eb97480e3f5621ff9a8da552f0961e7" + integrity sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ== + +"@types/d3-drag@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" + integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-dsv@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" + integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== + +"@types/d3-ease@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-fetch@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980" + integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-force@*": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.10.tgz#6dc8fc6e1f35704f3b057090beeeb7ac674bff1a" + integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw== + +"@types/d3-format@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90" + integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== + +"@types/d3-geo@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440" + integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b" + integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== + +"@types/d3-interpolate@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a" + integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== + +"@types/d3-polygon@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c" + integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== + +"@types/d3-quadtree@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f" + integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== + +"@types/d3-random@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb" + integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== + +"@types/d3-scale-chromatic@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39" + integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ== + +"@types/d3-scale@*": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb" + integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== dependencies: "@types/d3-time" "*" +"@types/d3-selection@*": + version "3.0.11" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3" + integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w== + +"@types/d3-shape@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" + integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2" + integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== + "@types/d3-time@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== +"@types/d3-timer@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + +"@types/d3-transition@*": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706" + integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@*": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" + integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@^7.4.3": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2" + integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-delaunay" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-zoom" "*" + "@types/debug@^4.0.0": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" @@ -2338,7 +4118,7 @@ dependencies: "@types/ms" "*" -"@types/eslint-scope@^3.7.3": +"@types/eslint-scope@^3.7.3", "@types/eslint-scope@^3.7.7": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== @@ -2366,6 +4146,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/estree@^1.0.6": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": version "4.19.3" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz#e469a13e4186c9e1c0418fb17be8bc8ff1b19a7a" @@ -2386,6 +4171,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/geojson@*": + version "7946.0.16" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" + integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== + "@types/gtag.js@^0.0.12": version "0.0.12" resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.12.tgz#095122edca896689bdfcdd73b057e23064d23572" @@ -2459,7 +4249,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -2512,11 +4302,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== -"@types/parse-json@^4.0.0": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" - integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== - "@types/parse5@^6.0.0": version "6.0.3" resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" @@ -2629,6 +4414,11 @@ dependencies: "@types/node" "*" +"@types/trusted-types@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + "@types/unist@*", "@types/unist@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" @@ -2678,21 +4468,44 @@ "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== + dependencies: + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/floating-point-hex-parser@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== + "@webassemblyjs/helper-api-error@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== + "@webassemblyjs/helper-buffer@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== + "@webassemblyjs/helper-numbers@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" @@ -2702,11 +4515,25 @@ "@webassemblyjs/helper-api-error" "1.11.6" "@xtuc/long" "4.2.2" +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" + "@xtuc/long" "4.2.2" + "@webassemblyjs/helper-wasm-bytecode@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== + "@webassemblyjs/helper-wasm-section@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" @@ -2717,6 +4544,16 @@ "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/wasm-gen" "1.12.1" +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/ieee754@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" @@ -2724,6 +4561,13 @@ dependencies: "@xtuc/ieee754" "^1.2.0" +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== + dependencies: + "@xtuc/ieee754" "^1.2.0" + "@webassemblyjs/leb128@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" @@ -2731,11 +4575,23 @@ dependencies: "@xtuc/long" "4.2.2" +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== + dependencies: + "@xtuc/long" "4.2.2" + "@webassemblyjs/utf8@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== + "@webassemblyjs/wasm-edit@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" @@ -2750,6 +4606,20 @@ "@webassemblyjs/wasm-parser" "1.12.1" "@webassemblyjs/wast-printer" "1.12.1" +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" + "@webassemblyjs/wasm-gen@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" @@ -2761,6 +4631,17 @@ "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + "@webassemblyjs/wasm-opt@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" @@ -2771,6 +4652,16 @@ "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/wasm-parser" "1.12.1" +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" @@ -2783,6 +4674,18 @@ "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + "@webassemblyjs/wast-printer@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" @@ -2791,6 +4694,14 @@ "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@xtuc/long" "4.2.2" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -2801,13 +4712,6 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -2838,7 +4742,12 @@ acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.7.1, acorn@^8.8.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c" integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw== -address@^1.0.1, address@^1.1.2: +acorn@^8.14.0: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + +address@^1.0.1: version "1.2.2" resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== @@ -2870,7 +4779,7 @@ ajv-formats@2.1.1, ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" -ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: +ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== @@ -2892,7 +4801,7 @@ ajv@8.11.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ajv@^6.12.2, ajv@^6.12.5: +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== @@ -2912,33 +4821,38 @@ ajv@^8.0.0, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.4.1" -algoliasearch-helper@^3.13.3: - version "3.21.0" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.21.0.tgz#d28fdb61199b5c229714788bfb812376b18aaf28" - integrity sha512-hjVOrL15I3Y3K8xG0icwG1/tWE+MocqBrhW6uVBWpU+/kVEMK0BnM2xdssj6mZM61eJ4iRxHR0djEI3ENOpR8w== +algoliasearch-helper@^3.22.6: + version "3.25.0" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.25.0.tgz#15cc79ad7909db66b8bb5a5a9c38b40e3941fa2f" + integrity sha512-vQoK43U6HXA9/euCqLjvyNdM4G2Fiu/VFp4ae0Gau9sZeIKBPvUPnXfLYAe65Bg7PFuw03coeu5K6lTPSXRObw== dependencies: "@algolia/events" "^4.0.1" -algoliasearch@^4.18.0, algoliasearch@^4.19.1: - version "4.23.3" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.23.3.tgz#e09011d0a3b0651444916a3e6bbcba064ec44b60" - integrity sha512-Le/3YgNvjW9zxIQMRhUHuhiUjAlKY/zsdZpfq4dlLqg6mEm0nL6yk+7f2hDOtLpxsgE4jSzDmvHL7nXdBp5feg== +algoliasearch@^5.14.2, algoliasearch@^5.17.1: + version "5.25.0" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.25.0.tgz#7337b097deadeca0e6e985c0f8724abea189994f" + integrity sha512-n73BVorL4HIwKlfJKb4SEzAYkR3Buwfwbh+MYxg2mloFph2fFGV58E90QTzdbfzWrLn4HE5Czx/WTjI8fcHaMg== dependencies: - "@algolia/cache-browser-local-storage" "4.23.3" - "@algolia/cache-common" "4.23.3" - "@algolia/cache-in-memory" "4.23.3" - "@algolia/client-account" "4.23.3" - "@algolia/client-analytics" "4.23.3" - "@algolia/client-common" "4.23.3" - "@algolia/client-personalization" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/logger-common" "4.23.3" - "@algolia/logger-console" "4.23.3" - "@algolia/recommend" "4.23.3" - "@algolia/requester-browser-xhr" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/requester-node-http" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-abtesting" "5.25.0" + "@algolia/client-analytics" "5.25.0" + "@algolia/client-common" "5.25.0" + "@algolia/client-insights" "5.25.0" + "@algolia/client-personalization" "5.25.0" + "@algolia/client-query-suggestions" "5.25.0" + "@algolia/client-search" "5.25.0" + "@algolia/ingestion" "1.25.0" + "@algolia/monitoring" "1.25.0" + "@algolia/recommend" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" + +allof-merge@^0.6.6: + version "0.6.6" + resolved "https://registry.yarnpkg.com/allof-merge/-/allof-merge-0.6.6.tgz#1c675c7170e1b24bd3dc96db9c3459c0e7cfbea2" + integrity sha512-116eZBf2he0/J4Tl7EYMz96I5Anaeio+VL0j/H2yxW9CoYQAMMv8gYcwkVRoO7XfIOv/qzSTfVzDVGAYxKFi3g== + dependencies: + json-crawl "^0.5.3" ansi-align@^3.0.1: version "3.0.1" @@ -2947,6 +4861,13 @@ ansi-align@^3.0.1: dependencies: string-width "^4.1.0" +ansi-escapes@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-html-community@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" @@ -3021,26 +4942,6 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -asn1.js@^4.10.1: - version "4.10.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" - integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -assert@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" - integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== - dependencies: - call-bind "^1.0.2" - is-nan "^1.3.2" - object-is "^1.1.5" - object.assign "^4.1.4" - util "^0.12.5" - astring@^1.8.0: version "1.8.6" resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.6.tgz#2c9c157cf1739d67561c56ba896e6948f6b93731" @@ -3061,7 +4962,7 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -autoprefixer@^10.4.13, autoprefixer@^10.4.14, autoprefixer@^10.4.19: +autoprefixer@^10.4.13, autoprefixer@^10.4.19: version "10.4.19" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew== @@ -3073,24 +4974,22 @@ autoprefixer@^10.4.13, autoprefixer@^10.4.14, autoprefixer@^10.4.19: picocolors "^1.0.0" postcss-value-parser "^4.2.0" -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== +autoprefixer@^10.4.21: + version "10.4.21" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.21.tgz#77189468e7a8ad1d9a37fbc08efc9f480cf0a95d" + integrity sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ== dependencies: - possible-typed-array-names "^1.0.0" + browserslist "^4.24.4" + caniuse-lite "^1.0.30001702" + fraction.js "^4.3.7" + normalize-range "^0.1.2" + picocolors "^1.1.1" + postcss-value-parser "^4.2.0" -axios@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" - integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== - dependencies: - follow-redirects "^1.14.7" - -babel-loader@^9.1.3: - version "9.1.3" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" - integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== +babel-loader@^9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.2.1.tgz#04c7835db16c246dd19ba0914418f3937797587b" + integrity sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA== dependencies: find-cache-dir "^4.0.0" schema-utils "^4.0.0" @@ -3111,7 +5010,7 @@ babel-plugin-polyfill-corejs2@^0.4.10: "@babel/helper-define-polyfill-provider" "^0.6.2" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.10.1, babel-plugin-polyfill-corejs3@^0.10.4: +babel-plugin-polyfill-corejs3@^0.10.4: version "0.10.4" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== @@ -3119,6 +5018,14 @@ babel-plugin-polyfill-corejs3@^0.10.1, babel-plugin-polyfill-corejs3@^0.10.4: "@babel/helper-define-polyfill-provider" "^0.6.1" core-js-compat "^3.36.1" +babel-plugin-polyfill-corejs3@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz#4e4e182f1bb37c7ba62e2af81d8dd09df31344f6" + integrity sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.3" + core-js-compat "^3.40.0" + babel-plugin-polyfill-regenerator@^0.6.1: version "0.6.2" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" @@ -3165,16 +5072,6 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.0.0, bn.js@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== - body-parser@1.20.2: version "1.20.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" @@ -3256,74 +5153,7 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== - -browserify-aes@^1.0.4, browserify-aes@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.3.tgz#7afe4c01ec7ee59a89a558a4b75bd85ae62d4208" - integrity sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw== - dependencies: - bn.js "^5.2.1" - browserify-rsa "^4.1.0" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.5" - hash-base "~3.0" - inherits "^2.0.4" - parse-asn1 "^5.1.7" - readable-stream "^2.3.8" - safe-buffer "^5.2.1" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.22.2, browserslist@^4.23.0: +browserslist@^4.0.0, browserslist@^4.21.10, browserslist@^4.22.2, browserslist@^4.23.0: version "4.23.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96" integrity sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw== @@ -3333,6 +5163,16 @@ browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^ node-releases "^2.0.14" update-browserslist-db "^1.0.16" +browserslist@^4.24.0, browserslist@^4.24.2, browserslist@^4.24.4, browserslist@^4.24.5: + version "4.24.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.5.tgz#aa0f5b8560fe81fde84c6dcb38f759bafba0e11b" + integrity sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw== + dependencies: + caniuse-lite "^1.0.30001716" + electron-to-chromium "^1.5.149" + node-releases "^2.0.19" + update-browserslist-db "^1.1.3" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -3343,11 +5183,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== - buffer@^5.2.1, buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -3364,11 +5199,6 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== - bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -3397,7 +5227,15 @@ cacheable-request@^10.2.8: normalize-url "^8.0.0" responselike "^3.0.0" -call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.7: +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.5, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== @@ -3408,6 +5246,14 @@ call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + call-me-maybe@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" @@ -3456,6 +5302,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001629: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz" integrity sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA== +caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001716, caniuse-lite@^1.0.30001718: + version "1.0.30001718" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz#dae13a9c80d517c30c6197515a96131c194d8f82" + integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw== + ccount@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" @@ -3470,7 +5321,7 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3525,7 +5376,7 @@ cheerio-select@^2.1.0: domhandler "^5.0.3" domutils "^3.0.1" -cheerio@^1.0.0-rc.12: +cheerio@1.0.0-rc.12: version "1.0.0-rc.12" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== @@ -3538,7 +5389,26 @@ cheerio@^1.0.0-rc.12: parse5 "^7.0.0" parse5-htmlparser2-tree-adapter "^7.0.0" -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3: +chevrotain-allstar@~0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz#b7412755f5d83cc139ab65810cdb00d8db40e6ca" + integrity sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw== + dependencies: + lodash-es "^4.17.21" + +chevrotain@~11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-11.0.3.tgz#88ffc1fb4b5739c715807eaeedbbf200e202fc1b" + integrity sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw== + dependencies: + "@chevrotain/cst-dts-gen" "11.0.3" + "@chevrotain/gast" "11.0.3" + "@chevrotain/regexp-to-ast" "11.0.3" + "@chevrotain/types" "11.0.3" + "@chevrotain/utils" "11.0.3" + lodash-es "4.17.21" + +chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -3553,6 +5423,13 @@ cheerio@^1.0.0-rc.12: optionalDependencies: fsevents "~2.3.2" +chokidar@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -3568,14 +5445,6 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - clean-css@^5.2.2, clean-css@^5.3.2, clean-css@~5.3.2: version "5.3.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" @@ -3768,6 +5637,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + +confbox@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.2.tgz#8652f53961c74d9e081784beed78555974a9c110" + integrity sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ== + config-chain@^1.1.11: version "1.1.13" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" @@ -3792,20 +5671,10 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== -consola@^2.15.3: - version "2.15.3" - resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" - integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== - -console-browserify@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== +consola@^3.2.3: + version "3.4.2" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" + integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== content-disposition@0.5.2: version "0.5.2" @@ -3870,6 +5739,13 @@ core-js-compat@^3.31.0, core-js-compat@^3.36.1: dependencies: browserslist "^4.23.0" +core-js-compat@^3.40.0: + version "3.42.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.42.0.tgz#ce19c29706ee5806e26d3cb3c542d4cfc0ed51bb" + integrity sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ== + dependencies: + browserslist "^4.24.4" + core-js-pure@^3.30.2: version "3.37.1" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.37.1.tgz#2b4b34281f54db06c9a9a5bd60105046900553bd" @@ -3892,16 +5768,12 @@ cose-base@^1.0.0: dependencies: layout-base "^1.0.0" -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== +cose-base@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-2.2.0.tgz#1c395c35b6e10bb83f9769ca8b817d614add5c01" + integrity sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g== dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" + layout-base "^2.0.0" cosmiconfig@^8.1.3, cosmiconfig@^8.3.5: version "8.3.6" @@ -3913,37 +5785,6 @@ cosmiconfig@^8.1.3, cosmiconfig@^8.3.5: parse-json "^5.2.0" path-type "^4.0.0" -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - cross-fetch@3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" @@ -3960,23 +5801,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-browserify@^3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - crypto-js@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" @@ -3989,11 +5813,27 @@ crypto-random-string@^4.0.0: dependencies: type-fest "^1.0.1" +css-blank-pseudo@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz#32020bff20a209a53ad71b8675852b49e8d57e46" + integrity sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag== + dependencies: + postcss-selector-parser "^7.0.0" + css-declaration-sorter@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz#6dec1c9523bc4a643e088aab8f09e67a54961024" integrity sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow== +css-has-pseudo@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-7.0.2.tgz#fb42e8de7371f2896961e1f6308f13c2c7019b72" + integrity sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-value-parser "^4.2.0" + css-loader@^6.8.1: version "6.11.0" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" @@ -4020,6 +5860,11 @@ css-minimizer-webpack-plugin@^5.0.1: schema-utils "^4.0.1" serialize-javascript "^6.0.1" +css-prefers-color-scheme@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz#ba001b99b8105b8896ca26fc38309ddb2278bd3c" + integrity sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ== + css-select@^4.1.3: version "4.3.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" @@ -4063,6 +5908,11 @@ css-what@^6.0.1, css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +cssdb@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.3.0.tgz#940becad497b8509ad822a28fb0cfe54c969ccfe" + integrity sha512-c7bmItIg38DgGjSwDPZOYF/2o0QU/sSgkWOMyl8votOfgFuyiFKWPesmCGEsrGLxEA9uL540cp8LdaGEjUGsZQ== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -4149,10 +5999,17 @@ cytoscape-cose-bilkent@^4.1.0: dependencies: cose-base "^1.0.0" -cytoscape@^3.28.1: - version "3.29.2" - resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.29.2.tgz#c99f42513c80a75e2e94858add32896c860202ac" - integrity sha512-2G1ycU28Nh7OHT9rkXRLpCDP30MKH1dXJORZuBhtEhEW7pKwgPi77ImqlCWinouyE1PNepIOGZBOrE84DG7LyQ== +cytoscape-fcose@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz#e4d6f6490df4fab58ae9cea9e5c3ab8d7472f471" + integrity sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ== + dependencies: + cose-base "^2.2.0" + +cytoscape@^3.29.3: + version "3.32.0" + resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.32.0.tgz#34bc2402c9bc7457ab7d9492745f034b7bf47644" + integrity sha512-5JHBC9n75kz5851jeklCPmZWcg3hUe6sjqJvyk3+hVqFaKcHwHgxsjeN1yLmggoUc6STbtm9/NQyabQehfjvWQ== "d3-array@1 - 2": version "2.12.1" @@ -4389,7 +6246,7 @@ d3-zoom@3: d3-selection "2 - 3" d3-transition "2 - 3" -d3@^7.4.0, d3@^7.8.2: +d3@^7.9.0: version "7.9.0" resolved "https://registry.yarnpkg.com/d3/-/d3-7.9.0.tgz#579e7acb3d749caf8860bd1741ae8d371070cd5d" integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA== @@ -4425,25 +6282,25 @@ d3@^7.4.0, d3@^7.8.2: d3-transition "3" d3-zoom "3" -dagre-d3-es@7.0.10: - version "7.0.10" - resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz#19800d4be674379a3cd8c86a8216a2ac6827cadc" - integrity sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A== +dagre-d3-es@7.0.11: + version "7.0.11" + resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz#2237e726c0577bfe67d1a7cfd2265b9ab2c15c40" + integrity sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw== dependencies: - d3 "^7.8.2" + d3 "^7.9.0" lodash-es "^4.17.21" -dayjs@^1.11.7: - version "1.11.11" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" - integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== +dayjs@^1.11.13: + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== debounce@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@2.6.9, debug@^2.6.0: +debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -4464,6 +6321,13 @@ debug@4.3.4: dependencies: ms "2.1.2" +debug@^4.4.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + decode-named-character-reference@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" @@ -4483,7 +6347,7 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deepmerge@^4.0.0, deepmerge@^4.2.2, deepmerge@^4.3.1: +deepmerge@^4.0.0, deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== @@ -4514,7 +6378,7 @@ 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== -define-properties@^1.1.3, define-properties@^1.2.1: +define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -4523,20 +6387,6 @@ define-properties@^1.1.3, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" -del@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" - integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== - dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" - rimraf "^3.0.2" - slash "^3.0.0" - delaunator@5: version "5.0.1" resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" @@ -4559,32 +6409,26 @@ dequal@^2.0.0: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== -des.js@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da" - integrity sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + +detect-libc@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" + integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== + detect-node@^2.0.4: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== -detect-port-alt@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" - integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== - dependencies: - address "^1.0.1" - debug "^2.6.0" - detect-port@^1.5.1: version "1.6.1" resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.6.1.tgz#45e4073997c5f292b957cb678fb0bb8ed4250a67" @@ -4615,15 +6459,6 @@ diff@^5.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -4643,29 +6478,26 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" -docusaurus-plugin-image-zoom@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-image-zoom/-/docusaurus-plugin-image-zoom-1.0.1.tgz#17afec39f2e630cac50a4ed3a8bbdad8d0aa8b9d" - integrity sha512-96IpSKUx2RWy3db9aZ0s673OQo5DWgV9UVWouS+CPOSIVEdCWh6HKmWf6tB9rsoaiIF3oNn9keiyv6neEyKb1Q== +docusaurus-plugin-image-zoom@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-image-zoom/-/docusaurus-plugin-image-zoom-3.0.1.tgz#76095fdc288b58d351d19bf902bd3c0a3113ec09" + integrity sha512-mQrqA99VpoMQJNbi02qkWAMVNC4+kwc6zLLMNzraHAJlwn+HrlUmZSEDcTwgn+H4herYNxHKxveE2WsYy73eGw== dependencies: - medium-zoom "^1.0.6" + medium-zoom "^1.1.0" validate-peer-dependencies "^2.2.0" -docusaurus-plugin-openapi-docs@3.0.1, docusaurus-plugin-openapi-docs@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-3.0.1.tgz#954fdc4103d7e47133aede210a98353b3e0f0f99" - integrity sha512-6SRqwey/TXMNu2G02mbWgxrifhpjGOjDr30N+58AR0Ytgc+HXMqlPAUIvTe+e7sOBfAtBbiNlmOWv5KSYIjf3w== +docusaurus-plugin-openapi-docs@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.4.0.tgz#010b2bfc57aeac1b62c41bf1ef386dcc52c5e91f" + integrity sha512-VFW0euAyM6i6U6Q2WrNXkp1LnxQFGszZbmloMFYrs1qwBjPLkuHfQ4OJMXGDsGcGl4zNDJ9cwODmJlmdwl1hwg== dependencies: "@apidevtools/json-schema-ref-parser" "^11.5.4" - "@docusaurus/plugin-content-docs" "^3.0.1" - "@docusaurus/utils" "^3.0.1" - "@docusaurus/utils-validation" "^3.0.1" "@redocly/openapi-core" "^1.10.5" + allof-merge "^0.6.6" chalk "^4.1.2" clsx "^1.1.1" fs-extra "^9.0.1" json-pointer "^0.6.2" - json-schema-merge-allof "^0.8.1" json5 "^2.2.3" lodash "^4.17.20" mustache "^4.2.0" @@ -4675,13 +6507,6 @@ docusaurus-plugin-openapi-docs@3.0.1, docusaurus-plugin-openapi-docs@^3.0.1: swagger2openapi "^7.0.8" xml-formatter "^2.6.1" -docusaurus-plugin-sass@^0.2.3: - version "0.2.5" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.5.tgz#6bfb8a227ac6265be685dcbc24ba1989e27b8005" - integrity sha512-Z+D0fLFUKcFpM+bqSUmqKIU+vO+YF1xoEQh5hoFreg2eMf722+siwXDD+sqtwU8E4MvVpuvsQfaHwODNlxJAEg== - dependencies: - sass-loader "^10.1.1" - docusaurus-theme-github-codeblock@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/docusaurus-theme-github-codeblock/-/docusaurus-theme-github-codeblock-2.0.2.tgz#88b7044b81f9091330e8e4a07a1bdc9114a9fb93" @@ -4689,25 +6514,25 @@ docusaurus-theme-github-codeblock@^2.0.2: dependencies: "@docusaurus/types" "^3.0.0" -docusaurus-theme-openapi-docs@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-3.0.1.tgz#49789c63377f294e624a9632eddb8265a421020f" - integrity sha512-tqypV91tC3wuWj9O+4n0M/e5AgHOeMT2nvPj1tjlPkC7/dLinZvpwQStT4YDUPYSoHRseqxd7lhivFQHcmlryg== +docusaurus-theme-openapi-docs@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.4.0.tgz#601eb34d43fa49c6fe1418f3fed06e3044ce377f" + integrity sha512-wmc2b946rqBcdjgEHi6Up7e8orasYk5RnIUerTfmZ/Hi006I8FIjMnJEmHAF6t5PbFiiYnlkB6vYK0CC5xBnCQ== dependencies: - "@docusaurus/theme-common" "^3.0.1" "@hookform/error-message" "^2.0.1" "@reduxjs/toolkit" "^1.7.1" + allof-merge "^0.6.6" + buffer "^6.0.3" clsx "^1.1.1" copy-text-to-clipboard "^3.1.0" crypto-js "^4.1.1" - docusaurus-plugin-openapi-docs "^3.0.1" - docusaurus-plugin-sass "^0.2.3" file-saver "^2.0.5" lodash "^4.17.20" - node-polyfill-webpack-plugin "^2.0.1" + pako "^2.1.0" postman-code-generators "^1.10.1" postman-collection "^4.4.0" prism-react-renderer "^2.3.0" + process "^0.11.10" react-hook-form "^7.43.8" react-live "^4.0.0" react-magic-dropzone "^1.0.1" @@ -4715,9 +6540,11 @@ docusaurus-theme-openapi-docs@3.0.1: react-modal "^3.15.1" react-redux "^7.2.0" rehype-raw "^6.1.1" - sass "^1.58.1" - sass-loader "^13.3.2" - webpack "^5.61.0" + remark-gfm "3.0.1" + sass "^1.80.4" + sass-loader "^16.0.2" + unist-util-visit "^5.0.0" + url "^0.11.1" xml-formatter "^2.6.1" dom-converter@^0.2.0: @@ -4745,11 +6572,6 @@ dom-serializer@^2.0.0: domhandler "^5.0.2" entities "^4.2.0" -domain-browser@^4.22.0: - version "4.23.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-4.23.0.tgz#427ebb91efcb070f05cffdfb8a4e9a6c25f8c94b" - integrity sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA== - domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" @@ -4769,10 +6591,12 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -dompurify@^3.0.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.5.tgz#2c6a113fc728682a0f55684b1388c58ddb79dc38" - integrity sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA== +dompurify@^3.2.4: + version "3.2.6" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad" + integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ== + optionalDependencies: + "@types/trusted-types" "^2.0.7" domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" @@ -4807,6 +6631,15 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -4827,23 +6660,10 @@ electron-to-chromium@^1.4.796: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz#cf55808a5ee12e2a2778bbe8cdc941ef87c2093b" integrity sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g== -elkjs@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.9.3.tgz#16711f8ceb09f1b12b99e971b138a8384a529161" - integrity sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ== - -elliptic@^6.5.3, elliptic@^6.5.5: - version "6.5.7" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" - integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" +electron-to-chromium@^1.5.149: + version "1.5.158" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.158.tgz#e5f01fc7fdf810d9d223e30593e0839c306276d4" + integrity sha512-9vcp2xHhkvraY6AHw2WMi+GDSLPX42qe2xjYaVoZqFRJiOcilVQFq9mZmpuHEQpzlgGDelKlV7ZiGcmMsc8WxQ== emoji-regex@^8.0.0: version "8.0.0" @@ -4890,6 +6710,14 @@ enhanced-resolve@^5.17.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.17.1: + version "5.18.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" + integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -4914,6 +6742,11 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" @@ -4924,6 +6757,13 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.3.tgz#25969419de9c0b1fbe54279789023e8a9a788412" integrity sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + es6-promise@^3.2.1: version "3.3.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" @@ -4934,6 +6774,11 @@ escalade@^3.1.1, escalade@^3.1.2: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-goat@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-4.0.0.tgz#9424820331b510b0666b98f7873fe11ac4aa8081" @@ -5095,30 +6940,17 @@ eval@^0.1.8: "@types/node" "*" require-like ">= 0.1.1" -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -eventemitter3@^4.0.0: +eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.2.0, events@^3.3.0: +events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -execa@^5.0.0: +execa@5.1.1, execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -5175,6 +7007,11 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +exsolve@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.5.tgz#1f5b6b4fe82ad6b28a173ccb955a635d77859dcf" + integrity sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg== + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -5229,13 +7066,6 @@ fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== -fast-url-parser@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" - integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== - dependencies: - punycode "^1.3.2" - fastq@^1.6.0: version "1.17.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" @@ -5271,6 +7101,13 @@ feed@^4.2.2: dependencies: xml-js "^1.6.11" +figures@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + file-loader@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" @@ -5289,11 +7126,6 @@ file-type@3.9.0: resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" integrity sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA== -filesize@^8.0.6: - version "8.0.7" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" - integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== - fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -5301,11 +7133,6 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -filter-obj@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-2.0.2.tgz#fff662368e505d69826abb113f0f6a98f56e9d5f" - integrity sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg== - finalhandler@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" @@ -5327,21 +7154,6 @@ find-cache-dir@^4.0.0: common-path-prefix "^3.0.0" pkg-dir "^7.0.0" -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - find-up@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" @@ -5355,18 +7167,11 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -follow-redirects@^1.0.0, follow-redirects@^1.14.7: +follow-redirects@^1.0.0: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - foreach@^2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.6.tgz#87bcc8a1a0e74000ff2bf9802110708cfb02eb6e" @@ -5380,25 +7185,6 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -fork-ts-checker-webpack-plugin@^6.5.0: - version "6.5.3" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3" - integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== - dependencies: - "@babel/code-frame" "^7.8.3" - "@types/json-schema" "^7.0.5" - chalk "^4.1.0" - chokidar "^3.4.2" - cosmiconfig "^6.0.0" - deepmerge "^4.2.2" - fs-extra "^9.0.0" - glob "^7.1.6" - memfs "^3.1.2" - minimatch "^3.0.4" - schema-utils "2.7.0" - semver "^7.3.2" - tapable "^1.0.0" - form-data-encoder@^2.1.2: version "2.1.4" resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" @@ -5438,7 +7224,7 @@ fs-extra@^11.1.1, fs-extra@^11.2.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^9.0.0, fs-extra@^9.0.1: +fs-extra@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -5489,11 +7275,35 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -5541,7 +7351,7 @@ glob@^10.3.10: minipass "^7.1.2" path-scurry "^1.11.1" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: +glob@^7.0.0, glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5560,28 +7370,17 @@ global-dirs@^3.0.0: dependencies: ini "2.0.0" -global-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: +globals@^15.14.0: + version "15.15.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8" + integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg== + +globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -5611,6 +7410,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + got@^12.1.0: version "12.6.1" resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" @@ -5662,6 +7466,11 @@ gzip-size@^6.0.0: dependencies: duplexer "^0.1.2" +hachure-fill@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/hachure-fill/-/hachure-fill-0.5.2.tgz#d19bc4cc8750a5962b47fb1300557a85fcf934cc" + integrity sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg== + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -5694,44 +7503,17 @@ has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-yarn@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-3.0.0.tgz#c3c21e559730d1d3b57e28af1f30d06fac38147d" integrity sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA== -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash-base@~3.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hasown@^2.0.0: +hasown@^2.0.0, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -5966,15 +7748,6 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -6043,10 +7816,10 @@ html-void-elements@^3.0.0: resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== -html-webpack-plugin@^5.5.3: - version "5.6.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" - integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== +html-webpack-plugin@^5.6.0: + version "5.6.3" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz#a31145f0fee4184d53a794f9513147df1e653685" + integrity sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg== dependencies: "@types/html-minifier-terser" "^6.0.0" html-minifier-terser "^6.0.2" @@ -6148,11 +7921,6 @@ http2-wrapper@^2.1.10: quick-lru "^5.1.1" resolve-alpn "^1.2.0" -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== - https-proxy-agent@5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -6195,24 +7963,22 @@ ignore@^5.2.0, ignore@^5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== -image-size@^1.0.2: - 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" +image-size@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-2.0.2.tgz#84a7b43704db5736f364bf0d1b029821299b4bdc" + integrity sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w== -immer@^9.0.21, immer@^9.0.7: +immer@^9.0.21: version "9.0.21" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== -immutable@^4.0.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447" - integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ== +immutable@^5.0.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.2.tgz#e8169476414505e5a4fa650107b65e1227d16d4b" + integrity sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ== -import-fresh@^3.1.0, import-fresh@^3.3.0: +import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -6235,10 +8001,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== -infima@0.2.0-alpha.43: - version "0.2.0-alpha.43" - resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.43.tgz#f7aa1d7b30b6c08afef441c726bac6150228cbe0" - integrity sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ== +infima@0.2.0-alpha.45: + version "0.2.0-alpha.45" + resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.45.tgz#542aab5a249274d81679631b492973dd2c1e7466" + integrity sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw== inflight@^1.0.4: version "1.0.6" @@ -6248,7 +8014,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -6263,7 +8029,7 @@ ini@2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: +ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -6323,14 +8089,6 @@ is-alphanumerical@^2.0.0: is-alphabetical "^2.0.0" is-decimal "^2.0.0" -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -6348,11 +8106,6 @@ is-buffer@^2.0.0: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.3: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - is-ci@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" @@ -6392,13 +8145,6 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -6419,14 +8165,6 @@ is-installed-globally@^0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" -is-nan@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" - integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - is-npm@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-6.0.0.tgz#b59e75e8915543ca5d881ecff864077cba095261" @@ -6447,11 +8185,6 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== -is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - is-path-inside@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -6486,23 +8219,11 @@ is-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== -is-root@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" - integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== - is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-typed-array@^1.1.3: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== - dependencies: - which-typed-array "^1.1.14" - is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -6585,7 +8306,7 @@ jiti@^1.20.0, jiti@^1.21.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== -joi@^17.6.0, joi@^17.9.2: +joi@^17.9.2: version "17.13.1" resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.1.tgz#9c7b53dc3b44dd9ae200255cc3b398874918a6ca" integrity sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg== @@ -6626,16 +8347,31 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== +json-crawl@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/json-crawl/-/json-crawl-0.5.3.tgz#3a2e1d308d4fc5a444902f1f94f4a9e03d584c6b" + integrity sha512-BEjjCw8c7SxzNK4orhlWD5cXQh8vCk2LqDr4WgQq4CV+5dvopeYwt1Tskg67SuSLKvoFH5g0yuYtg7rcfKV6YA== + json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -6655,7 +8391,7 @@ json-schema-compare@^0.2.2: dependencies: lodash "^4.17.4" -json-schema-merge-allof@0.8.1, json-schema-merge-allof@^0.8.1: +json-schema-merge-allof@0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz#ed2828cdd958616ff74f932830a26291789eaaf2" integrity sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w== @@ -6702,7 +8438,7 @@ keyv@^4.5.3: dependencies: json-buffer "3.0.1" -khroma@^2.0.0: +khroma@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.1.0.tgz#45f2ce94ce231a437cf5b63c2e886e6eb42bbbb1" integrity sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw== @@ -6722,10 +8458,21 @@ kleur@^4.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== -klona@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" - integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== +kolorist@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c" + integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== + +langium@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/langium/-/langium-3.3.1.tgz#da745a40d5ad8ee565090fed52eaee643be4e591" + integrity sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w== + dependencies: + chevrotain "~11.0.3" + chevrotain-allstar "~0.3.0" + vscode-languageserver "~9.0.1" + vscode-languageserver-textdocument "~1.0.11" + vscode-uri "~3.0.8" latest-version@^7.0.0: version "7.0.0" @@ -6747,11 +8494,84 @@ layout-base@^1.0.0: resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-1.0.2.tgz#1291e296883c322a9dd4c5dd82063721b53e26e2" integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg== +layout-base@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-2.0.1.tgz#d0337913586c90f9c2c075292069f5c2da5dd285" + integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg== + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== +lightningcss-darwin-arm64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz#3d47ce5e221b9567c703950edf2529ca4a3700ae" + integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ== + +lightningcss-darwin-x64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz#e81105d3fd6330860c15fe860f64d39cff5fbd22" + integrity sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA== + +lightningcss-freebsd-x64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz#a0e732031083ff9d625c5db021d09eb085af8be4" + integrity sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig== + +lightningcss-linux-arm-gnueabihf@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz#1f5ecca6095528ddb649f9304ba2560c72474908" + integrity sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q== + +lightningcss-linux-arm64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz#eee7799726103bffff1e88993df726f6911ec009" + integrity sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw== + +lightningcss-linux-arm64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz#f2e4b53f42892feeef8f620cbb889f7c064a7dfe" + integrity sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ== + +lightningcss-linux-x64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz#2fc7096224bc000ebb97eea94aea248c5b0eb157" + integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw== + +lightningcss-linux-x64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz#66dca2b159fd819ea832c44895d07e5b31d75f26" + integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ== + +lightningcss-win32-arm64-msvc@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz#7d8110a19d7c2d22bfdf2f2bb8be68e7d1b69039" + integrity sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA== + +lightningcss-win32-x64-msvc@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz#fd7dd008ea98494b85d24b4bea016793f2e0e352" + integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg== + +lightningcss@^1.27.0: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.30.1.tgz#78e979c2d595bfcb90d2a8c0eb632fe6c5bfed5d" + integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-darwin-arm64 "1.30.1" + lightningcss-darwin-x64 "1.30.1" + lightningcss-freebsd-x64 "1.30.1" + lightningcss-linux-arm-gnueabihf "1.30.1" + lightningcss-linux-arm64-gnu "1.30.1" + lightningcss-linux-arm64-musl "1.30.1" + lightningcss-linux-x64-gnu "1.30.1" + lightningcss-linux-x64-musl "1.30.1" + lightningcss-win32-arm64-msvc "1.30.1" + lightningcss-win32-x64-msvc "1.30.1" + lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -6791,25 +8611,14 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -loader-utils@^3.2.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5" - integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== +local-pkg@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-1.1.1.tgz#f5fe74a97a3bd3c165788ee08ca9fbe998dc58dd" + integrity sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg== dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" + mlly "^1.7.4" + pkg-types "^2.0.1" + quansync "^0.2.8" locate-path@^7.1.0: version "7.2.0" @@ -6818,7 +8627,7 @@ locate-path@^7.1.0: dependencies: p-locate "^6.0.0" -lodash-es@^4.17.21: +lodash-es@4.17.21, lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== @@ -6896,19 +8705,27 @@ markdown-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4" integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q== +markdown-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" + integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== + dependencies: + repeat-string "^1.0.0" + markdown-table@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" +marked@^15.0.7: + version "15.0.12" + resolved "https://registry.yarnpkg.com/marked/-/marked-15.0.12.tgz#30722c7346e12d0a2d0207ab9b0c4f0102d86c4e" + integrity sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== mdast-util-definitions@^5.0.0: version "5.1.2" @@ -6933,6 +8750,16 @@ mdast-util-directive@^3.0.0: stringify-entities "^4.0.0" unist-util-visit-parents "^6.0.0" +mdast-util-find-and-replace@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz#cc2b774f7f3630da4bd592f61966fecade8b99b1" + integrity sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw== + dependencies: + "@types/mdast" "^3.0.0" + escape-string-regexp "^5.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.0.0" + mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz#a6fc7b62f0994e973490e45262e4bc07607b04e0" @@ -6943,7 +8770,7 @@ mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: unist-util-is "^6.0.0" unist-util-visit-parents "^6.0.0" -mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.1.0, mdast-util-from-markdown@^1.2.0, mdast-util-from-markdown@^1.3.0: +mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.1.0, mdast-util-from-markdown@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== @@ -6991,6 +8818,16 @@ mdast-util-frontmatter@^2.0.0: mdast-util-to-markdown "^2.0.0" micromark-extension-frontmatter "^2.0.0" +mdast-util-gfm-autolink-literal@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz#67a13abe813d7eba350453a5333ae1bc0ec05c06" + integrity sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA== + dependencies: + "@types/mdast" "^3.0.0" + ccount "^2.0.0" + mdast-util-find-and-replace "^2.0.0" + micromark-util-character "^1.0.0" + mdast-util-gfm-autolink-literal@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz#5baf35407421310a08e68c15e5d8821e8898ba2a" @@ -7002,6 +8839,15 @@ mdast-util-gfm-autolink-literal@^2.0.0: mdast-util-find-and-replace "^3.0.0" micromark-util-character "^2.0.0" +mdast-util-gfm-footnote@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz#ce5e49b639c44de68d5bf5399877a14d5020424e" + integrity sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + micromark-util-normalize-identifier "^1.0.0" + mdast-util-gfm-footnote@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz#25a1753c7d16db8bfd53cd84fe50562bd1e6d6a9" @@ -7013,6 +8859,14 @@ mdast-util-gfm-footnote@^2.0.0: mdast-util-to-markdown "^2.0.0" micromark-util-normalize-identifier "^2.0.0" +mdast-util-gfm-strikethrough@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz#5470eb105b483f7746b8805b9b989342085795b7" + integrity sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + mdast-util-gfm-strikethrough@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16" @@ -7022,6 +8876,16 @@ mdast-util-gfm-strikethrough@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" +mdast-util-gfm-table@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz#3552153a146379f0f9c4c1101b071d70bbed1a46" + integrity sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg== + dependencies: + "@types/mdast" "^3.0.0" + markdown-table "^3.0.0" + mdast-util-from-markdown "^1.0.0" + mdast-util-to-markdown "^1.3.0" + mdast-util-gfm-table@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38" @@ -7033,6 +8897,14 @@ mdast-util-gfm-table@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" +mdast-util-gfm-task-list-item@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz#b280fcf3b7be6fd0cc012bbe67a59831eb34097b" + integrity sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + mdast-util-gfm-task-list-item@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936" @@ -7043,6 +8915,19 @@ mdast-util-gfm-task-list-item@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" +mdast-util-gfm@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6" + integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg== + dependencies: + mdast-util-from-markdown "^1.0.0" + mdast-util-gfm-autolink-literal "^1.0.0" + mdast-util-gfm-footnote "^1.0.0" + mdast-util-gfm-strikethrough "^1.0.0" + mdast-util-gfm-table "^1.0.0" + mdast-util-gfm-task-list-item "^1.0.0" + mdast-util-to-markdown "^1.0.0" + mdast-util-gfm@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz#3f2aecc879785c3cb6a81ff3a243dc11eca61095" @@ -7277,12 +9162,12 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== -medium-zoom@^1.0.6: +medium-zoom@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/medium-zoom/-/medium-zoom-1.1.0.tgz#6efb6bbda861a02064ee71a2617a8dc4381ecc71" integrity sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ== -memfs@^3.1.2, memfs@^3.4.3: +memfs@^3.4.3: version "3.6.0" resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== @@ -7309,31 +9194,31 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -mermaid@^10.4.0, mermaid@^10.9.1: - version "10.9.1" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.9.1.tgz#5f582c23f3186c46c6aa673e59eeb46d741b2ea6" - integrity sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA== +mermaid@>=11.6.0: + version "11.6.0" + resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-11.6.0.tgz#eee45cdc3087be561a19faf01745596d946bb575" + integrity sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg== dependencies: - "@braintree/sanitize-url" "^6.0.1" - "@types/d3-scale" "^4.0.3" - "@types/d3-scale-chromatic" "^3.0.0" - cytoscape "^3.28.1" + "@braintree/sanitize-url" "^7.0.4" + "@iconify/utils" "^2.1.33" + "@mermaid-js/parser" "^0.4.0" + "@types/d3" "^7.4.3" + cytoscape "^3.29.3" cytoscape-cose-bilkent "^4.1.0" - d3 "^7.4.0" + cytoscape-fcose "^2.2.0" + d3 "^7.9.0" d3-sankey "^0.12.3" - dagre-d3-es "7.0.10" - dayjs "^1.11.7" - dompurify "^3.0.5" - elkjs "^0.9.0" + dagre-d3-es "7.0.11" + dayjs "^1.11.13" + dompurify "^3.2.4" katex "^0.16.9" - khroma "^2.0.0" + khroma "^2.1.0" lodash-es "^4.17.21" - mdast-util-from-markdown "^1.3.0" - non-layered-tidy-tree-layout "^2.0.2" - stylis "^4.1.3" + marked "^15.0.7" + roughjs "^4.6.6" + stylis "^4.3.6" ts-dedent "^2.2.0" - uuid "^9.0.0" - web-worker "^1.2.0" + uuid "^11.1.0" methods@~1.1.2: version "1.1.2" @@ -7407,6 +9292,16 @@ micromark-extension-frontmatter@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-autolink-literal@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz#5853f0e579bbd8ef9e39a7c0f0f27c5a063a66e7" + integrity sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-extension-gfm-autolink-literal@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz#f1e50b42e67d441528f39a67133eddde2bbabfd9" @@ -7417,6 +9312,20 @@ micromark-extension-gfm-autolink-literal@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-footnote@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz#05e13034d68f95ca53c99679040bc88a6f92fe2e" + integrity sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q== + dependencies: + micromark-core-commonmark "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-extension-gfm-footnote@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz#91afad310065a94b636ab1e9dab2c60d1aab953c" @@ -7431,6 +9340,18 @@ micromark-extension-gfm-footnote@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-strikethrough@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz#c8212c9a616fa3bf47cb5c711da77f4fdc2f80af" + integrity sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-extension-gfm-strikethrough@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz#6917db8e320da70e39ffbf97abdbff83e6783e61" @@ -7443,6 +9364,17 @@ micromark-extension-gfm-strikethrough@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-table@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz#dcb46074b0c6254c3fc9cc1f6f5002c162968008" + integrity sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-extension-gfm-table@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz#2cf3fe352d9e089b7ef5fff003bdfe0da29649b7" @@ -7454,6 +9386,13 @@ micromark-extension-gfm-table@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-tagfilter@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz#aa7c4dd92dabbcb80f313ebaaa8eb3dac05f13a7" + integrity sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g== + dependencies: + micromark-util-types "^1.0.0" + micromark-extension-gfm-tagfilter@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57" @@ -7461,6 +9400,17 @@ micromark-extension-gfm-tagfilter@^2.0.0: dependencies: micromark-util-types "^2.0.0" +micromark-extension-gfm-task-list-item@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz#b52ce498dc4c69b6a9975abafc18f275b9dde9f4" + integrity sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-extension-gfm-task-list-item@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz#ee8b208f1ced1eb9fb11c19a23666e59d86d4838" @@ -7472,6 +9422,20 @@ micromark-extension-gfm-task-list-item@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz#e517e8579949a5024a493e49204e884aa74f5acf" + integrity sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ== + dependencies: + micromark-extension-gfm-autolink-literal "^1.0.0" + micromark-extension-gfm-footnote "^1.0.0" + micromark-extension-gfm-strikethrough "^1.0.0" + micromark-extension-gfm-table "^1.0.0" + micromark-extension-gfm-tagfilter "^1.0.0" + micromark-extension-gfm-task-list-item "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-types "^1.0.0" + micromark-extension-gfm@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b" @@ -8026,14 +9990,6 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.3" picomatch "^2.3.1" -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - mime-db@1.48.0: version "1.48.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" @@ -8097,25 +10053,20 @@ mimic-response@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== -mini-css-extract-plugin@^2.7.6: - version "2.9.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz#c73a1327ccf466f69026ac22a8e8fd707b78a235" - integrity sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA== +mini-css-extract-plugin@^2.9.1: + version "2.9.2" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz#966031b468917a5446f4c24a80854b2947503c5b" + integrity sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w== dependencies: schema-utils "^4.0.0" tapable "^2.2.1" -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: +minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== - -minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: +minimatch@3.1.2, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -8136,7 +10087,7 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.2.0: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -8151,6 +10102,16 @@ mkdirp-classic@^0.5.2: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== +mlly@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.4.tgz#3d7295ea2358ec7a271eaa5d000a0f84febe100f" + integrity sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw== + dependencies: + acorn "^8.14.0" + pathe "^2.0.1" + pkg-types "^1.3.0" + ufo "^1.5.4" + mri@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" @@ -8171,7 +10132,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3: +ms@2.1.3, 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== @@ -8221,6 +10182,11 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + node-emoji@^2.1.0: version "2.1.3" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-2.1.3.tgz#93cfabb5cc7c3653aa52f29d6ffb7927d8047c06" @@ -8257,37 +10223,6 @@ node-forge@^1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== -node-polyfill-webpack-plugin@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-2.0.1.tgz#141d86f177103a8517c71d99b7c6a46edbb1bb58" - integrity sha512-ZUMiCnZkP1LF0Th2caY6J/eKKoA0TefpoVa68m/LQU1I/mE8rGt4fNYGgNuCcK+aG8P8P43nbeJ2RqJMOL/Y1A== - dependencies: - assert "^2.0.0" - browserify-zlib "^0.2.0" - buffer "^6.0.3" - console-browserify "^1.2.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.12.0" - domain-browser "^4.22.0" - events "^3.3.0" - filter-obj "^2.0.2" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "^1.0.1" - process "^0.11.10" - punycode "^2.1.1" - querystring-es3 "^0.2.1" - readable-stream "^4.0.0" - stream-browserify "^3.0.0" - stream-http "^3.2.0" - string_decoder "^1.3.0" - timers-browserify "^2.0.12" - tty-browserify "^0.0.1" - type-fest "^2.14.0" - url "^0.11.0" - util "^0.12.4" - vm-browserify "^1.1.2" - node-readfiles@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/node-readfiles/-/node-readfiles-0.2.0.tgz#dbbd4af12134e2e635c245ef93ffcf6f60673a5d" @@ -8300,10 +10235,10 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== -non-layered-tidy-tree-layout@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804" - integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw== +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -8339,6 +10274,14 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" +null-loader@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-4.0.1.tgz#8e63bd3a2dd3c64236a4679428632edd0a6dbc6a" + integrity sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + oas-kit-common@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/oas-kit-common/-/oas-kit-common-1.0.8.tgz#6d8cacf6e9097967a4c7ea8bcbcbd77018e1f535" @@ -8412,20 +10355,17 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== -object-is@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" - integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.0, object.assign@^4.1.4: +object.assign@^4.1.0: version "4.1.5" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== @@ -8502,29 +10442,15 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== - p-cancelable@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== -p-limit@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== p-limit@^4.0.0: version "4.0.0" @@ -8533,20 +10459,6 @@ p-limit@^4.0.0: dependencies: yocto-queue "^1.0.0" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - p-locate@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" @@ -8561,6 +10473,14 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" +p-queue@^6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + p-retry@^4.5.0: version "4.6.2" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" @@ -8569,10 +10489,12 @@ p-retry@^4.5.0: "@types/retry" "0.12.0" retry "^0.13.1" -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" package-json@^8.1.0: version "8.1.1" @@ -8584,10 +10506,15 @@ package-json@^8.1.0: registry-url "^6.0.0" semver "^7.3.7" -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== +package-manager-detector@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/package-manager-detector/-/package-manager-detector-1.3.0.tgz#b42d641c448826e03c2b354272456a771ce453c0" + integrity sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ== + +pako@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== param-case@^3.0.4: version "3.0.4" @@ -8604,18 +10531,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0, parse-asn1@^5.1.7: - version "5.1.7" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.7.tgz#73cdaaa822125f9647165625eb45f8a051d2df06" - integrity sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg== - dependencies: - asn1.js "^4.10.1" - browserify-aes "^1.2.0" - evp_bytestokey "^1.0.3" - hash-base "~3.0" - pbkdf2 "^3.1.2" - safe-buffer "^5.2.1" - parse-entities@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" @@ -8630,7 +10545,7 @@ parse-entities@^4.0.0: is-decimal "^2.0.0" is-hexadecimal "^2.0.0" -parse-json@^5.0.0, parse-json@^5.2.0: +parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -8683,15 +10598,10 @@ path-browserify@1.0.1, path-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== +path-data-parser@0.1.0, path-data-parser@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/path-data-parser/-/path-data-parser-0.1.0.tgz#8f5ba5cc70fc7becb3dcefaea08e2659aba60b8c" + integrity sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w== path-exists@^5.0.0: version "5.0.0" @@ -8743,10 +10653,10 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== -path-to-regexp@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" - integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== +path-to-regexp@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" + integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== path-to-regexp@^1.7.0: version "1.8.0" @@ -8768,16 +10678,10 @@ path@0.12.7: process "^0.11.1" util "^0.10.3" -pbkdf2@^3.0.3, pbkdf2@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" - integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" +pathe@^2.0.1, pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== pend@~1.2.0: version "1.2.0" @@ -8798,6 +10702,11 @@ picocolors@^1.0.0, picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -8820,22 +10729,48 @@ pkg-dir@^7.0.0: dependencies: find-up "^6.3.0" -pkg-up@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" - integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== +pkg-types@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.3.1.tgz#bd7cc70881192777eef5326c19deb46e890917df" + integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== dependencies: - find-up "^3.0.0" + confbox "^0.1.8" + mlly "^1.7.4" + pathe "^2.0.1" + +pkg-types@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-2.1.0.tgz#70c9e1b9c74b63fdde749876ee0aa007ea9edead" + integrity sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A== + dependencies: + confbox "^0.2.1" + exsolve "^1.0.1" + pathe "^2.0.3" pluralize@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== -possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== +points-on-curve@0.2.0, points-on-curve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/points-on-curve/-/points-on-curve-0.2.0.tgz#7dbb98c43791859434284761330fa893cb81b4d1" + integrity sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A== + +points-on-path@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/points-on-path/-/points-on-path-0.2.1.tgz#553202b5424c53bed37135b318858eacff85dd52" + integrity sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g== + dependencies: + path-data-parser "0.1.0" + points-on-curve "0.2.0" + +postcss-attribute-case-insensitive@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz#0c4500e3bcb2141848e89382c05b5a31c23033a3" + integrity sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw== + dependencies: + postcss-selector-parser "^7.0.0" postcss-calc@^9.0.1: version "9.0.1" @@ -8845,6 +10780,40 @@ postcss-calc@^9.0.1: postcss-selector-parser "^6.0.11" postcss-value-parser "^4.2.0" +postcss-clamp@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz#7263e95abadd8c2ba1bd911b0b5a5c9c93e02363" + integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-functional-notation@^7.0.10: + version "7.0.10" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.10.tgz#f1e9c3e4371889dcdfeabfa8515464fd8338cedc" + integrity sha512-k9qX+aXHBiLTRrWoCJuUFI6F1iF6QJQUXNVWJVSbqZgj57jDhBlOvD8gNUGl35tgqDivbGLhZeW3Ongz4feuKA== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +postcss-color-hex-alpha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz#5dd3eba1f8facb4ea306cba6e3f7712e876b0c76" + integrity sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-color-rebeccapurple@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz#5ada28406ac47e0796dff4056b0a9d5a6ecead98" + integrity sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + postcss-colormin@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-6.1.0.tgz#076e8d3fb291fbff7b10e6b063be9da42ff6488d" @@ -8863,6 +10832,44 @@ postcss-convert-values@^6.1.0: browserslist "^4.23.0" postcss-value-parser "^4.2.0" +postcss-custom-media@^11.0.6: + version "11.0.6" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz#6b450e5bfa209efb736830066682e6567bd04967" + integrity sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.5" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" + +postcss-custom-properties@^14.0.5: + version "14.0.5" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-14.0.5.tgz#a180444de695f6e11ee2390be93ff6537663e86c" + integrity sha512-UWf/vhMapZatv+zOuqlfLmYXeOhhHLh8U8HAKGI2VJ00xLRYoAJh4xv8iX6FB6+TLXeDnm0DBLMi00E0hodbQw== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.5" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-custom-selectors@^8.0.5: + version "8.0.5" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz#9448ed37a12271d7ab6cb364b6f76a46a4a323e8" + integrity sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.5" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + postcss-selector-parser "^7.0.0" + +postcss-dir-pseudo-class@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz#80d9e842c9ae9d29f6bf5fd3cf9972891d6cc0ca" + integrity sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-discard-comments@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz#e768dcfdc33e0216380623652b0a4f69f4678b6c" @@ -8890,6 +10897,47 @@ postcss-discard-unused@^6.0.5: dependencies: postcss-selector-parser "^6.0.16" +postcss-double-position-gradients@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.2.tgz#185f8eab2db9cf4e34be69b5706c905895bb52ae" + integrity sha512-7qTqnL7nfLRyJK/AHSVrrXOuvDDzettC+wGoienURV8v2svNbu6zJC52ruZtHaO6mfcagFmuTGFdzRsJKB3k5Q== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-focus-visible@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz#1f7904904368a2d1180b220595d77b6f8a957868" + integrity sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-focus-within@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz#ac01ce80d3f2e8b2b3eac4ff84f8e15cd0057bc7" + integrity sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-font-variant@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" + integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== + +postcss-gap-properties@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz#d5ff0bdf923c06686499ed2b12e125fe64054fed" + integrity sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw== + +postcss-image-set-function@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz#538e94e16716be47f9df0573b56bbaca86e1da53" + integrity sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + postcss-import@^15.1.0: version "15.1.0" resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" @@ -8906,6 +10954,17 @@ postcss-js@^4.0.1: dependencies: camelcase-css "^2.0.1" +postcss-lab-function@^7.0.10: + version "7.0.10" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-7.0.10.tgz#0537bd7245b935fc133298c8896bcbd160540cae" + integrity sha512-tqs6TCEv9tC1Riq6fOzHuHcZyhg4k3gIAMB8GGY/zA1ssGdm6puHMVE7t75aOSoFg7UD2wyrFFhbldiCMyyFTQ== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + postcss-load-config@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" @@ -8923,6 +10982,13 @@ postcss-loader@^7.3.3: jiti "^1.20.0" semver "^7.5.4" +postcss-logical@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-8.1.0.tgz#4092b16b49e3ecda70c4d8945257da403d167228" + integrity sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA== + dependencies: + postcss-value-parser "^4.2.0" + postcss-merge-idents@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz#7b9c31c7bc823c94bec50f297f04e3c2b838ea65" @@ -9016,6 +11082,15 @@ postcss-nested@^6.0.1: dependencies: postcss-selector-parser "^6.0.11" +postcss-nesting@^13.0.1: + version "13.0.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-13.0.1.tgz#c405796d7245a3e4c267a9956cacfe9670b5d43e" + integrity sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ== + dependencies: + "@csstools/selector-resolve-nested" "^3.0.0" + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-normalize-charset@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz#1ec25c435057a8001dac942942a95ffe66f721e1" @@ -9078,6 +11153,11 @@ postcss-normalize-whitespace@^6.0.2: dependencies: postcss-value-parser "^4.2.0" +postcss-opacity-percentage@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz#0b0db5ed5db5670e067044b8030b89c216e1eb0a" + integrity sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ== + postcss-ordered-values@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz#366bb663919707093451ab70c3f99c05672aaae5" @@ -9086,6 +11166,102 @@ postcss-ordered-values@^6.0.2: cssnano-utils "^4.0.2" postcss-value-parser "^4.2.0" +postcss-overflow-shorthand@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz#f5252b4a2ee16c68cd8a9029edb5370c4a9808af" + integrity sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-page-break@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" + integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== + +postcss-place@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-10.0.0.tgz#ba36ee4786ca401377ced17a39d9050ed772e5a9" + integrity sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-preset-env@^10.1.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-10.2.0.tgz#ea95a6fc70efb1a26f81e5cf0ccdb321a3439b6e" + integrity sha512-cl13sPBbSqo1Q7Ryb19oT5NZO5IHFolRbIMdgDq4f9w1MHYiL6uZS7uSsjXJ1KzRIcX5BMjEeyxmAevVXENa3Q== + dependencies: + "@csstools/postcss-cascade-layers" "^5.0.1" + "@csstools/postcss-color-function" "^4.0.10" + "@csstools/postcss-color-mix-function" "^3.0.10" + "@csstools/postcss-color-mix-variadic-function-arguments" "^1.0.0" + "@csstools/postcss-content-alt-text" "^2.0.6" + "@csstools/postcss-exponential-functions" "^2.0.9" + "@csstools/postcss-font-format-keywords" "^4.0.0" + "@csstools/postcss-gamut-mapping" "^2.0.10" + "@csstools/postcss-gradients-interpolation-method" "^5.0.10" + "@csstools/postcss-hwb-function" "^4.0.10" + "@csstools/postcss-ic-unit" "^4.0.2" + "@csstools/postcss-initial" "^2.0.1" + "@csstools/postcss-is-pseudo-class" "^5.0.1" + "@csstools/postcss-light-dark-function" "^2.0.9" + "@csstools/postcss-logical-float-and-clear" "^3.0.0" + "@csstools/postcss-logical-overflow" "^2.0.0" + "@csstools/postcss-logical-overscroll-behavior" "^2.0.0" + "@csstools/postcss-logical-resize" "^3.0.0" + "@csstools/postcss-logical-viewport-units" "^3.0.4" + "@csstools/postcss-media-minmax" "^2.0.9" + "@csstools/postcss-media-queries-aspect-ratio-number-values" "^3.0.5" + "@csstools/postcss-nested-calc" "^4.0.0" + "@csstools/postcss-normalize-display-values" "^4.0.0" + "@csstools/postcss-oklab-function" "^4.0.10" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-random-function" "^2.0.1" + "@csstools/postcss-relative-color-syntax" "^3.0.10" + "@csstools/postcss-scope-pseudo-class" "^4.0.1" + "@csstools/postcss-sign-functions" "^1.1.4" + "@csstools/postcss-stepped-value-functions" "^4.0.9" + "@csstools/postcss-text-decoration-shorthand" "^4.0.2" + "@csstools/postcss-trigonometric-functions" "^4.0.9" + "@csstools/postcss-unset-value" "^4.0.0" + autoprefixer "^10.4.21" + browserslist "^4.24.5" + css-blank-pseudo "^7.0.1" + css-has-pseudo "^7.0.2" + css-prefers-color-scheme "^10.0.0" + cssdb "^8.3.0" + postcss-attribute-case-insensitive "^7.0.1" + postcss-clamp "^4.1.0" + postcss-color-functional-notation "^7.0.10" + postcss-color-hex-alpha "^10.0.0" + postcss-color-rebeccapurple "^10.0.0" + postcss-custom-media "^11.0.6" + postcss-custom-properties "^14.0.5" + postcss-custom-selectors "^8.0.5" + postcss-dir-pseudo-class "^9.0.1" + postcss-double-position-gradients "^6.0.2" + postcss-focus-visible "^10.0.1" + postcss-focus-within "^9.0.1" + postcss-font-variant "^5.0.0" + postcss-gap-properties "^6.0.0" + postcss-image-set-function "^7.0.0" + postcss-lab-function "^7.0.10" + postcss-logical "^8.1.0" + postcss-nesting "^13.0.1" + postcss-opacity-percentage "^3.0.0" + postcss-overflow-shorthand "^6.0.0" + postcss-page-break "^3.0.4" + postcss-place "^10.0.0" + postcss-pseudo-class-any-link "^10.0.1" + postcss-replace-overflow-wrap "^4.0.0" + postcss-selector-not "^8.0.1" + +postcss-pseudo-class-any-link@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz#06455431171bf44b84d79ebaeee9fd1c05946544" + integrity sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-reduce-idents@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz#b0d9c84316d2a547714ebab523ec7d13704cd486" @@ -9108,6 +11284,18 @@ postcss-reduce-transforms@^6.0.2: dependencies: postcss-value-parser "^4.2.0" +postcss-replace-overflow-wrap@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" + integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== + +postcss-selector-not@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz#f2df9c6ac9f95e9fe4416ca41a957eda16130172" + integrity sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.1.0" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53" @@ -9116,6 +11304,14 @@ postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-select cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz#4d6af97eba65d73bc4d84bcb343e865d7dd16262" + integrity sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-sort-media-queries@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz#4556b3f982ef27d3bac526b99b6c0d3359a6cf97" @@ -9246,7 +11442,7 @@ pretty-time@^1.1.0: resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== -prism-react-renderer@^2.0.6, prism-react-renderer@^2.1.0, prism-react-renderer@^2.3.0: +prism-react-renderer@^2.0.6, prism-react-renderer@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-2.3.1.tgz#e59e5450052ede17488f6bc85de1553f584ff8d5" integrity sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw== @@ -9314,18 +11510,6 @@ proxy-from-env@1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -9334,7 +11518,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^1.3.2, punycode@^1.4.1: +punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== @@ -9384,50 +11568,35 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -qs@^6.11.2: - version "6.12.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" - integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== +qs@^6.12.3: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== dependencies: - side-channel "^1.0.6" + side-channel "^1.1.0" -querystring-es3@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA== +quansync@^0.2.8: + version "0.2.10" + resolved "https://registry.yarnpkg.com/quansync/-/quansync-0.2.10.tgz#32053cf166fa36511aae95fc49796116f2dc20e1" + integrity sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A== queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -queue@6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" - integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== - dependencies: - inherits "~2.0.3" - quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: +randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - range-parser@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" @@ -9474,36 +11643,6 @@ react-copy-to-clipboard@^5.1.0: copy-to-clipboard "^3.3.1" prop-types "^15.8.1" -react-dev-utils@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" - integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== - dependencies: - "@babel/code-frame" "^7.16.0" - address "^1.1.2" - browserslist "^4.18.1" - chalk "^4.1.2" - cross-spawn "^7.0.3" - detect-port-alt "^1.1.6" - escape-string-regexp "^4.0.0" - filesize "^8.0.6" - find-up "^5.0.0" - fork-ts-checker-webpack-plugin "^6.5.0" - global-modules "^2.0.0" - globby "^11.0.4" - gzip-size "^6.0.0" - immer "^9.0.7" - is-root "^2.1.0" - loader-utils "^3.2.0" - open "^8.4.0" - pkg-up "^3.1.0" - prompts "^2.4.2" - react-error-overlay "^6.0.11" - recursive-readdir "^2.2.2" - shell-quote "^1.7.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - react-dom@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" @@ -9512,12 +11651,7 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.2" -react-error-overlay@^6.0.11: - version "6.0.11" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" - integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== - -react-fast-compare@^3.0.1, react-fast-compare@^3.2.0, react-fast-compare@^3.2.2: +react-fast-compare@^3.0.1, react-fast-compare@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== @@ -9527,19 +11661,10 @@ react-google-charts@^5.2.1: resolved "https://registry.yarnpkg.com/react-google-charts/-/react-google-charts-5.2.1.tgz#d9cbe8ed45d7c0fafefea5c7c3361bee76648454" integrity sha512-mCbPiObP8yWM5A9ogej7Qp3/HX4EzOwuEzUYvcfHtL98Xt4V/brD14KgfDzSNNtyD48MNXCpq5oVaYKt0ykQUQ== -react-helmet-async@*: - version "2.0.5" - resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-2.0.5.tgz#cfc70cd7bb32df7883a8ed55502a1513747223ec" - integrity sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg== - dependencies: - invariant "^2.2.4" - react-fast-compare "^3.2.2" - shallowequal "^1.1.0" - -react-helmet-async@^1.3.0: +react-helmet-async@^1.3.0, "react-helmet-async@npm:@slorber/react-helmet-async@1.3.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.3.0.tgz#7bd5bf8c5c69ea9f02f6083f14ce33ef545c222e" - integrity sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg== + resolved "https://registry.yarnpkg.com/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz#11fbc6094605cf60aa04a28c17e0aab894b4ecff" + integrity sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A== dependencies: "@babel/runtime" "^7.12.5" invariant "^2.2.4" @@ -9567,10 +11692,10 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-json-view-lite@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-1.4.0.tgz#0ff493245f4550abe5e1f1836f170fa70bb95914" - integrity sha512-wh6F6uJyYAmQ4fK0e8dSQMEWuvTs2Wr3el3sLD9bambX1+pSWUVXIz1RFaoy3TI1mZ0FqdpKq9YgbgTTgyrmXA== +react-json-view-lite@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-2.4.1.tgz#0d06696a06aaf4a74e890302b76cf8cddcc45d60" + integrity sha512-fwFYknRIBxjbFm0kBDrzgBy1xa5tDg2LyXXBepC5f1b+MY3BUClMCsvanMPn089JbV1Eg3nZcrp0VCuH43aXnA== react-lifecycles-compat@^3.0.0: version "3.0.4" @@ -9708,7 +11833,7 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -readable-stream@^2.0.1, readable-stream@^2.3.8: +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== @@ -9721,7 +11846,7 @@ readable-stream@^2.0.1, readable-stream@^2.3.8: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -9730,16 +11855,10 @@ readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^4.0.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" - integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== readdirp@~3.6.0: version "3.6.0" @@ -9748,11 +11867,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -reading-time@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/reading-time/-/reading-time-1.5.0.tgz#d2a7f1b6057cb2e169beaf87113cc3411b5bc5bb" - integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== - rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -9760,13 +11874,6 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -recursive-readdir@^2.2.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" - integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== - dependencies: - minimatch "^3.0.5" - redux-thunk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" @@ -9791,6 +11898,13 @@ regenerate-unicode-properties@^10.1.0: dependencies: regenerate "^1.4.2" +regenerate-unicode-properties@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" + integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== + dependencies: + regenerate "^1.4.2" + regenerate@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" @@ -9820,6 +11934,18 @@ regexpu-core@^5.3.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" +regexpu-core@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" + integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.12.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + registry-auth-token@^5.0.1: version "5.0.2" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.2.tgz#8b026cc507c8552ebbe06724136267e63302f756" @@ -9834,6 +11960,18 @@ registry-url@^6.0.0: dependencies: rc "1.2.8" +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.12.0.tgz#0e846df6c6530586429377de56e0475583b088dc" + integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== + dependencies: + jsesc "~3.0.2" + regjsparser@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" @@ -9895,6 +12033,16 @@ remark-frontmatter@^5.0.0: micromark-extension-frontmatter "^2.0.0" unified "^11.0.0" +remark-gfm@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" + integrity sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-gfm "^2.0.0" + micromark-extension-gfm "^2.0.0" + unified "^10.0.0" + remark-gfm@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.0.tgz#aea777f0744701aa288b67d28c43565c7e8c35de" @@ -9975,6 +12123,11 @@ renderkid@^3.0.0: lodash "^4.17.21" strip-ansi "^6.0.1" +repeat-string@^1.0.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -10055,23 +12208,20 @@ rimraf@3.0.2, rimraf@^3.0.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - robust-predicates@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== -rtl-detect@^1.0.4: - version "1.1.2" - resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.1.2.tgz#ca7f0330af5c6bb626c15675c642ba85ad6273c6" - integrity sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ== +roughjs@^4.6.6: + version "4.6.6" + resolved "https://registry.yarnpkg.com/roughjs/-/roughjs-4.6.6.tgz#1059f49a5e0c80dee541a005b20cc322b222158b" + integrity sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ== + dependencies: + hachure-fill "^0.5.2" + path-data-parser "^0.1.0" + points-on-curve "^0.2.0" + points-on-path "^0.2.1" rtlcss@^4.1.0: version "4.1.1" @@ -10095,13 +12245,6 @@ rw@1: resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== -rxjs@^7.5.4: - 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" - sade@^1.7.3: version "1.8.1" resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" @@ -10114,7 +12257,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 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.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, 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== @@ -10124,32 +12267,23 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass-loader@^10.1.1: - version "10.5.2" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.5.2.tgz#1ca30534fff296417b853c7597ca3b0bbe8c37d0" - integrity sha512-vMUoSNOUKJILHpcNCCyD23X34gve1TS7Rjd9uXHeKqhvBG39x6XbswFDtpbTElj6XdMFezoWhkh5vtKudf2cgQ== - dependencies: - klona "^2.0.4" - loader-utils "^2.0.0" - neo-async "^2.6.2" - schema-utils "^3.0.0" - semver "^7.3.2" - -sass-loader@^13.3.2: - version "13.3.3" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-13.3.3.tgz#60df5e858788cffb1a3215e5b92e9cba61e7e133" - integrity sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA== +sass-loader@^16.0.2: + version "16.0.5" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-16.0.5.tgz#257bc90119ade066851cafe7f2c3f3504c7cda98" + integrity sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw== dependencies: neo-async "^2.6.2" -sass@^1.58.1: - version "1.77.5" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.5.tgz#5f9009820297521356e962c0bed13ee36710edfe" - integrity sha512-oDfX1mukIlxacPdQqNb6mV2tVCrnE+P3nVYioy72V5tlk56CPNcO4TCuFcaCRKKfJ1M3lH95CleRS+dVKL2qMg== +sass@^1.80.4: + version "1.89.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.89.0.tgz#6df72360c5c3ec2a9833c49adafe57b28206752d" + integrity sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ== dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" + chokidar "^4.0.0" + immutable "^5.0.2" source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" sax@^1.2.4: version "1.4.1" @@ -10163,14 +12297,10 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" -schema-utils@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" +schema-dts@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/schema-dts/-/schema-dts-1.1.5.tgz#9237725d305bac3469f02b292a035107595dc324" + integrity sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg== schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" @@ -10191,6 +12321,16 @@ schema-utils@^4.0.0, schema-utils@^4.0.1: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +schema-utils@^4.3.0, schema-utils@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" + integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + section-matter@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" @@ -10238,7 +12378,7 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4: +semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== @@ -10262,25 +12402,24 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: +serialize-javascript@^6.0.0, serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" -serve-handler@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375" - integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg== +serve-handler@^6.1.6: + version "6.1.6" + resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1" + integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ== dependencies: bytes "3.0.0" content-disposition "0.5.2" - fast-url-parser "1.1.3" mime-types "2.1.18" minimatch "3.1.2" path-is-inside "1.0.2" - path-to-regexp "2.2.1" + path-to-regexp "3.3.0" range-parser "1.2.0" serve-index@^1.9.1: @@ -10318,11 +12457,6 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" -setimmediate@^1.0.4: - 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" @@ -10333,14 +12467,6 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -10365,12 +12491,12 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.7.3, shell-quote@^1.8.1: +shell-quote@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -shelljs@0.8.5, shelljs@^0.8.5: +shelljs@0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== @@ -10423,7 +12549,36 @@ should@^13.2.1: should-type-adaptors "^1.0.1" should-util "^1.0.0" -side-channel@^1.0.4, side-channel@^1.0.6: +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== @@ -10433,6 +12588,17 @@ side-channel@^1.0.4, side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -10457,16 +12623,6 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -sitemap@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.1.1.tgz#eeed9ad6d95499161a3eadc60f8c6dce4bea2bef" - integrity sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg== - dependencies: - "@types/node" "^17.0.5" - "@types/sax" "^1.2.1" - arg "^5.0.0" - sax "^1.2.4" - sitemap@^7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.1.2.tgz#6ce1deb43f6f177c68bc59cf93632f54e3ae6b72" @@ -10592,28 +12748,10 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -std-env@^3.0.1: - version "3.7.0" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" - integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== - -stream-browserify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" - integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== - dependencies: - inherits "~2.0.4" - readable-stream "^3.5.0" - -stream-http@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.2.0.tgz#1872dfcf24cb15752677e40e5c3f9cc1926028b5" - integrity sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.4" - readable-stream "^3.6.0" - xtend "^4.0.2" +std-env@^3.7.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1" + integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw== "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" @@ -10642,7 +12780,7 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string_decoder@^1.1.1, string_decoder@^1.3.0: +string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -10736,10 +12874,10 @@ stylehacks@^6.1.1: browserslist "^4.23.0" postcss-selector-parser "^6.0.16" -stylis@^4.1.3: - version "4.3.2" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" - integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== +stylis@^4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.6.tgz#7c7b97191cb4f195f03ecab7d52f7902ed378320" + integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ== sucrase@^3.31.0, sucrase@^3.32.0: version "3.35.0" @@ -10815,7 +12953,7 @@ swagger2openapi@7.0.8, swagger2openapi@^7.0.8: yaml "^1.10.0" yargs "^17.0.1" -swc-loader@^0.2.3: +swc-loader@^0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/swc-loader/-/swc-loader-0.2.6.tgz#bf0cba8eeff34bb19620ead81d1277fefaec6bc8" integrity sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg== @@ -10850,11 +12988,6 @@ tailwindcss@^3.2.4: resolve "^1.22.2" sucrase "^3.32.0" -tapable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -10892,6 +13025,17 @@ terser-webpack-plugin@^5.3.10, terser-webpack-plugin@^5.3.9: serialize-javascript "^6.0.1" terser "^5.26.0" +terser-webpack-plugin@^5.3.11: + version "5.3.14" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06" + integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + jest-worker "^27.4.5" + schema-utils "^4.3.0" + serialize-javascript "^6.0.2" + terser "^5.31.1" + terser@^5.10.0, terser@^5.15.1, terser@^5.26.0: version "5.31.1" resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.1.tgz#735de3c987dd671e95190e6b98cfe2f07f3cf0d4" @@ -10902,10 +13046,15 @@ terser@^5.10.0, terser@^5.15.1, terser@^5.26.0: commander "^2.20.0" source-map-support "~0.5.20" -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +terser@^5.31.1: + version "5.40.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.40.0.tgz#839a80db42bfee8340085f44ea99b5cba36c55c8" + integrity sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.14.0" + commander "^2.20.0" + source-map-support "~0.5.20" thenify-all@^1.0.0: version "1.6.0" @@ -10931,13 +13080,6 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -timers-browserify@^2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" - integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== - dependencies: - setimmediate "^1.0.4" - tiny-invariant@^1.0.2: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" @@ -10948,6 +13090,16 @@ tiny-warning@^1.0.0: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tinyexec@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.1.tgz#70c31ab7abbb4aea0a24f55d120e5990bfa1e0b1" + integrity sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw== + +tinypool@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" + integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -11005,22 +13157,22 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -tslib@^2.0.3, tslib@^2.1.0, tslib@^2.6.0: +tslib@^2.0.3, tslib@^2.6.0: version "2.6.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== -tty-browserify@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" - integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== +type-fest@^0.21.3: + version "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@^1.0.1: version "1.4.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== -type-fest@^2.13.0, type-fest@^2.14.0, type-fest@^2.5.0: +type-fest@^2.13.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== @@ -11040,6 +13192,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +ufo@^1.5.4: + version "1.6.1" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b" + integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== + unbzip2-stream@1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" @@ -11191,7 +13348,7 @@ unist-util-stringify-position@^4.0.0: dependencies: "@types/unist" "^3.0.0" -unist-util-visit-parents@^5.1.1: +unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: version "5.1.3" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== @@ -11243,6 +13400,14 @@ update-browserslist-db@^1.0.16: escalade "^3.1.2" picocolors "^1.0.1" +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + update-notifier@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-6.0.2.tgz#a6990253dfe6d5a02bd04fbb6a61543f55026b60" @@ -11279,13 +13444,13 @@ url-loader@^4.1.1: mime-types "^2.1.27" schema-utils "^3.0.0" -url@^0.11.0: - version "0.11.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.3.tgz#6f495f4b935de40ce4a0a52faee8954244f3d3ad" - integrity sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw== +url@^0.11.1: + version "0.11.4" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.4.tgz#adca77b3562d56b72746e76b330b7f27b6721f3c" + integrity sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg== dependencies: punycode "^1.4.1" - qs "^6.11.2" + qs "^6.12.3" use-editable@^2.3.3: version "2.3.3" @@ -11304,17 +13469,6 @@ util@^0.10.3: dependencies: inherits "2.0.3" -util@^0.12.4, util@^0.12.5: - version "0.12.5" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" @@ -11335,10 +13489,10 @@ uuid@8.3.2, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== +uuid@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== uvu@^0.5.0: version "0.5.6" @@ -11449,21 +13603,40 @@ vfile@^6.0.0, vfile@^6.0.1: unist-util-stringify-position "^4.0.0" vfile-message "^4.0.0" -vm-browserify@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== -wait-on@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.1.tgz#16bbc4d1e4ebdd41c5b4e63a2e16dbd1f4e5601e" - integrity sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw== +vscode-languageserver-protocol@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== dependencies: - axios "^0.25.0" - joi "^17.6.0" - lodash "^4.17.21" - minimist "^1.2.5" - rxjs "^7.5.4" + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" + +vscode-languageserver-textdocument@~1.0.11: + version "1.0.12" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz#457ee04271ab38998a093c68c2342f53f6e4a631" + integrity sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA== + +vscode-languageserver-types@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + +vscode-languageserver@~9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz#500aef82097eb94df90d008678b0b6b5f474015b" + integrity sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g== + dependencies: + vscode-languageserver-protocol "3.17.5" + +vscode-uri@~3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== warning@^4.0.3: version "4.0.3" @@ -11492,17 +13665,12 @@ web-namespaces@^2.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== -web-worker@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.3.0.tgz#e5f2df5c7fe356755a5fb8f8410d4312627e6776" - integrity sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA== - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== -webpack-bundle-analyzer@^4.9.0: +webpack-bundle-analyzer@^4.10.2: version "4.10.2" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz#633af2862c213730be3dbdf40456db171b60d5bd" integrity sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw== @@ -11531,7 +13699,7 @@ webpack-dev-middleware@^5.3.4: range-parser "^1.2.1" schema-utils "^4.0.0" -webpack-dev-server@^4.15.1: +webpack-dev-server@^4.15.2: version "4.15.2" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== @@ -11576,12 +13744,21 @@ webpack-merge@^5.9.0: flat "^5.0.2" wildcard "^2.0.0" +webpack-merge@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a" + integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.1" + webpack-sources@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.61.0, webpack@^5.88.1: +webpack@^5.88.1: version "5.92.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.92.0.tgz#cc114c71e6851d220b1feaae90159ed52c876bdf" integrity sha512-Bsw2X39MYIgxouNATyVpCNVWBCuUwDgWtN78g6lSdPJRLaQ/PUVm/oXcaRAyY/sMFoKFQrsPeqvTizWtq7QPCA== @@ -11611,15 +13788,49 @@ webpack@^5.61.0, webpack@^5.88.1: watchpack "^2.4.1" webpack-sources "^3.2.3" -webpackbar@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570" - integrity sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ== +webpack@^5.95.0: + version "5.99.9" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.9.tgz#d7de799ec17d0cce3c83b70744b4aedb537d8247" + integrity sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg== dependencies: - chalk "^4.1.0" - consola "^2.15.3" + "@types/eslint-scope" "^3.7.7" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" + acorn "^8.14.0" + browserslist "^4.24.0" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^4.3.2" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.11" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +webpackbar@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-6.0.1.tgz#5ef57d3bf7ced8b19025477bc7496ea9d502076b" + integrity sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q== + dependencies: + ansi-escapes "^4.3.2" + chalk "^4.1.2" + consola "^3.2.3" + figures "^3.2.0" + markdown-table "^2.0.0" pretty-time "^1.1.0" - std-env "^3.0.1" + std-env "^3.7.0" + wrap-ansi "^7.0.0" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" @@ -11643,24 +13854,6 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -which-typed-array@^1.1.14, which-typed-array@^1.1.2: - version "1.1.15" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.2" - -which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -11675,7 +13868,7 @@ widest-line@^4.0.1: dependencies: string-width "^5.0.1" -wildcard@^2.0.0: +wildcard@^2.0.0, wildcard@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== @@ -11761,11 +13954,6 @@ xml-parser-xo@^3.2.0: resolved "https://registry.yarnpkg.com/xml-parser-xo/-/xml-parser-xo-3.2.0.tgz#c633ab55cf1976d6b03ab4a6a85045093ac32b73" integrity sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg== -xtend@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -11786,7 +13974,7 @@ yaml-ast-parser@0.0.43: resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== -yaml@1.10.2, yaml@^1.10.0, yaml@^1.7.2: +yaml@1.10.2, yaml@^1.10.0: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== @@ -11822,11 +14010,6 @@ yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" diff --git a/go.mod b/go.mod index de0e8aa314..09dd32e822 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,12 @@ module github.com/zitadel/zitadel -go 1.23.7 +go 1.24 + +toolchain go1.24.1 require ( cloud.google.com/go/profiler v0.4.2 - cloud.google.com/go/storage v1.51.0 + cloud.google.com/go/storage v1.54.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.27.0 @@ -20,18 +22,19 @@ require ( github.com/crewjam/saml v0.4.14 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/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0 github.com/drone/envsubst v1.0.3 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/gabriel-vasile/mimetype v1.4.9 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-jose/go-jose/v4 v4.1.0 + github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-webauthn/webauthn v0.10.2 github.com/goccy/go-json v0.10.5 github.com/golang/protobuf v1.5.4 + github.com/google/go-cmp v0.7.0 github.com/gorilla/csrf v1.7.2 github.com/gorilla/mux v1.8.1 github.com/gorilla/schema v1.4.1 @@ -43,38 +46,41 @@ require ( 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.3 + github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e + github.com/jackc/pgx/v5 v5.7.5 github.com/jackc/tern/v2 v2.3.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/minio/minio-go/v7 v7.0.88 + github.com/minio/minio-go/v7 v7.0.91 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.6.0 - github.com/pquerna/otp v1.4.0 + github.com/pashagolub/pgxmock/v4 v4.7.0 + github.com/pquerna/otp v1.5.0 github.com/rakyll/statik v0.1.7 - 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/redis/go-redis/v9 v9.8.0 + github.com/riverqueue/river v0.22.0 + github.com/riverqueue/river/riverdriver v0.22.0 + github.com/riverqueue/river/rivertype v0.22.0 + github.com/riverqueue/rivercontrib/otelriver v0.5.0 github.com/rs/cors v1.11.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 + github.com/shopspring/decimal v1.4.0 github.com/sony/gobreaker/v2 v2.1.0 - github.com/sony/sonyflake v1.2.0 + github.com/sony/sonyflake v1.2.1 github.com/spf13/cobra v1.9.1 - github.com/spf13/viper v1.20.0 + github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 github.com/ttacon/libphonenumber v1.2.1 - github.com/twilio/twilio-go v1.24.1 + github.com/twilio/twilio-go v1.26.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/oidc/v3 v3.37.0 + github.com/zitadel/passwap v0.9.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 @@ -87,27 +93,27 @@ require ( 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.37.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.13.0 - golang.org/x/text v0.24.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 + go.uber.org/mock v0.5.2 + golang.org/x/crypto v0.38.0 + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 + golang.org/x/net v0.40.0 + golang.org/x/oauth2 v0.30.0 + golang.org/x/sync v0.14.0 + golang.org/x/text v0.25.0 + google.golang.org/api v0.233.0 + google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 + google.golang.org/grpc v1.72.1 + google.golang.org/protobuf v1.36.6 sigs.k8s.io/yaml v1.4.0 ) require ( - 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 + cel.dev/expr v0.20.0 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/monitoring v1.24.0 // indirect dario.cat/mergo v1.0.1 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.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/Masterminds/goutils v1.1.1 // indirect @@ -136,7 +142,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // 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 @@ -148,30 +154,31 @@ require ( github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect - github.com/riverqueue/river/rivershared v0.19.0 // indirect + github.com/riverqueue/river/rivershared v0.22.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.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/zeebo/errs v1.4.0 // 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.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // 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 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect ) require ( - cloud.google.com/go v0.118.3 // indirect + cloud.google.com/go v0.121.0 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect - cloud.google.com/go/iam v1.4.1 // indirect + cloud.google.com/go/iam v1.5.2 // 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 @@ -194,7 +201,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // 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-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // 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.3 // indirect @@ -210,7 +217,7 @@ require ( 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.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect @@ -222,7 +229,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/riverqueue/river/riverdriver/riverpgxv5 v0.19.0 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.22.0 github.com/rs/xid v1.6.0 // indirect github.com/russellhaering/goxmldsig v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 @@ -235,7 +242,7 @@ require ( github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // 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.32.0 + golang.org/x/sys v0.33.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 ff10718898..d511119796 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,27 @@ -cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4= -cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= +cel.dev/expr v0.20.0/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.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 v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg= +cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= 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/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= 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/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= 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/storage v1.54.0 h1:Du3XEyliAiftfyW0bwfdppm2MMLdpVAfiIg4T2nAI+0= +cloud.google.com/go/storage v1.54.0/go.mod h1:hIi9Boe8cHxTyaeqh7KMMwKg088VblFK46C2x/BWaZE= 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= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= @@ -35,8 +35,8 @@ github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2 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/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/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= 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= @@ -163,8 +163,8 @@ github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yA 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/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0 h1:fuHXpEVTTk7TilRdfGRLHpiTD6tnT0ihEowCfWjlFvw= +github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0/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-20230826092837-6579e82b732d h1:ygcRCGNKuEiA98k7X35hknEN8RIRUF1jrz7k1rZCvsk= @@ -231,13 +231,13 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ 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/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 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.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-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/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= @@ -248,14 +248,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.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= 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.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= -github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= +github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= +github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= 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= @@ -304,7 +304,6 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a 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.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= @@ -348,7 +347,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw 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.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -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= @@ -384,10 +382,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 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/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -424,7 +420,6 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -449,14 +444,16 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= -github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 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.3 h1:PO1wNKj/bTAwxSJnO1Z4Ai8j4magtqg2SLNjEDzcXQo= -github.com/jackc/pgx/v5 v5.7.3/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e h1:i3gQ/Zo7sk4LUVbsAjTNeC4gIjoPNIZVzs4EXstssV4= +github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e/go.mod h1:zUHglCZ4mpDUPgIwqEKoba6+tcUQzRdb1+DPTuYe9pI= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= 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/jackc/tern/v2 v2.3.3 h1:d6QNRyjk9HttJtSF5pUB8UaXrHwCgEai3/yxYjgci/k= @@ -508,11 +505,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.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -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/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 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= @@ -563,8 +560,8 @@ 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.88 h1:v8MoIJjwYxOkehp+eiLIuvXk87P2raUtoU5klrAAshs= -github.com/minio/minio-go/v7 v7.0.88/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg= +github.com/minio/minio-go/v7 v7.0.91 h1:tWLZnEfo3OZl5PoXQwcwTAPNNrjyWwOh6cbZitW5JQc= +github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -628,8 +625,8 @@ 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.6.0 h1:ds0hIs+bJtkfo01vqjp0BOFirjt4Ea8XV082uorzM3w= -github.com/pashagolub/pgxmock/v4 v4.6.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A= +github.com/pashagolub/pgxmock/v4 v4.7.0 h1:de2ORuFYyjwOQR7NBm57+321RnZxpYiuUjsmqRiqgh8= +github.com/pashagolub/pgxmock/v4 v4.7.0/go.mod h1:9L57pC193h2aKRHVyiiE817avasIPZnPwPlw3JczWvM= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 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= @@ -648,8 +645,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN 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= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= -github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= +github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= @@ -683,22 +680,24 @@ 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.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= -github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= 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/riverqueue/river v0.22.0 h1:PO4Ula2RqViQqNs6xjze7yFV6Zq4T3Ffv092+f4S8xQ= +github.com/riverqueue/river v0.22.0/go.mod h1:IRoWoK4RGCiPuVJUV4EWcCl9d/TMQYkk0EEYV/Wgq+U= +github.com/riverqueue/river/riverdriver v0.22.0 h1:i7OSFkUi6x4UKvttdFOIg7NYLYaBOFLJZvkZ0+JWS/8= +github.com/riverqueue/river/riverdriver v0.22.0/go.mod h1:oNdjJCeAJhN/UiZGLNL+guNqWaxMFuSD4lr5x/v/was= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.22.0 h1:+no3gToOK9SmWg0pDPKfOGSCsrxqqaFdD8K1NQndRbY= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.22.0/go.mod h1:mygiHa1dnlKRjxT1//wIvfT2fMTbfXKm37NcsxoyBoQ= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.22.0 h1:2TWbVL73gipJ2/4JNCQbifaNj+BCC/Zxpp30o1D8RTg= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.22.0/go.mod h1:TZY/BG8w/nDxkraAEvvgyVupIz0b4+PQVUW0kIiy1fc= +github.com/riverqueue/river/rivershared v0.22.0 h1:hLPHr98d6OEfmUJ4KpIXgoy2tbQ14htWILcRBHJF11U= +github.com/riverqueue/river/rivershared v0.22.0/go.mod h1:BK+hvhECfdDLWNDH3xiGI95m2YoPfVtECZLT+my8XM8= +github.com/riverqueue/river/rivertype v0.22.0 h1:rSRhbd5uV/BaFTPxReCxuYTAzx+/riBZJlZdREADvO4= +github.com/riverqueue/river/rivertype v0.22.0/go.mod h1:lmdl3vLNDfchDWbYdW2uAocIuwIN+ZaXqAukdSCFqWs= +github.com/riverqueue/rivercontrib/otelriver v0.5.0 h1:dZF4Fy7/3RaIRsyCPdpIJtzEip0pCvoJ44YpSDum8e4= +github.com/riverqueue/rivercontrib/otelriver v0.5.0/go.mod h1:rXANcBrlgRvg+auD3/O6Xfs59AWeWNpa/kim62mkxGo= 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= @@ -741,8 +740,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= 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/sony/sonyflake v1.2.1 h1:Jzo4abS84qVNbYamXZdrZF1/6TzNJjEogRfXv7TsG48= +github.com/sony/sonyflake v1.2.1/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.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= @@ -756,23 +755,20 @@ github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= 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= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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/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= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -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.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= @@ -794,8 +790,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.24.1 h1:bpBL1j5GRdJGSG+tCdo0O94BwK4uDOHQuNT5ndzljPg= -github.com/twilio/twilio-go v1.24.1/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko= +github.com/twilio/twilio-go v1.26.1 h1:HazQUV+BCuW5CaJVMTjqV22V32LirwZNQBu98ADPQzM= +github.com/twilio/twilio-go v1.26.1/go.mod h1:FpgNWMoD8CFnmukpKq9RNpUSGXC0BwnbeKZj2YHlIkw= 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= @@ -812,19 +808,20 @@ github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQut github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= 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/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/oidc/v3 v3.37.0 h1:nYATWlnP7f18XiAbw6upUruBaqfB1kUrXrSTf1EYGO8= +github.com/zitadel/oidc/v3 v3.37.0/go.mod h1:/xDan4OUQhguJ4Ur73OOJrtugvR164OMnidXP9xfVNw= +github.com/zitadel/passwap v0.9.0 h1:QvDK8OHKdb73C0m+mwXvu87UJSBqix3oFwTVENHdv80= +github.com/zitadel/passwap v0.9.0/go.mod h1:6QzwFjDkIr3FfudzSogTOx5Ydhq4046dRJtDM/kX+G8= 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= @@ -836,8 +833,8 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 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/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= 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= @@ -850,8 +847,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+ 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/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= 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= @@ -871,8 +868,8 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -891,19 +888,13 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 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-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/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= 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= @@ -919,11 +910,6 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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= @@ -942,7 +928,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -954,24 +939,15 @@ 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-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= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -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.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/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 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.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 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= @@ -980,14 +956,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.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= @@ -1018,44 +988,20 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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.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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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.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= 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.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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 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.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= @@ -1080,17 +1026,13 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 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.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc= -google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ= +google.golang.org/api v0.233.0 h1:iGZfjXAJiUFSSaekVB7LzXl6tRfEKhUN7FkZN++07tI= +google.golang.org/api v0.233.0/go.mod h1:TCIVLLlcwunlMpZIhIp7Ltk77W+vUSdUKAAIlbxY44c= 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= @@ -1105,10 +1047,10 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 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/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 h1:WvBuA5rjZx9SNIzgcU53OohgZy6lKSus++uY4xLaWKc= +google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 h1:IqsN8hx+lWLqlN+Sc3DoMy/watjofWiU8sRFgQ8fhKM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 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= @@ -1123,8 +1065,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.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 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= @@ -1135,8 +1077,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.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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= diff --git a/internal/admin/repository/eventsourcing/handler/handler.go b/internal/admin/repository/eventsourcing/handler/handler.go index ec268c25a1..b38e890e66 100644 --- a/internal/admin/repository/eventsourcing/handler/handler.go +++ b/internal/admin/repository/eventsourcing/handler/handler.go @@ -2,9 +2,15 @@ package handler import ( "context" + "errors" + "fmt" "time" + "github.com/jackc/pgx/v5/pgconn" + "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view" + "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/handler/v2" @@ -57,11 +63,21 @@ func Start(ctx context.Context) { } func ProjectInstance(ctx context.Context) error { - for _, projection := range projections { - _, err := projection.Trigger(ctx) - if err != nil { - return err + for i, projection := range projections { + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting admin projection") + for { + _, err := projection.Trigger(ctx) + if err == nil { + break + } + var pgErr *pgconn.PgError + errors.As(err, &pgErr) + if pgErr.Code != database.PgUniqueConstraintErrorCode { + return err + } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("admin projection failed because of unique constraint, retrying") } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("admin projection done") } return nil } diff --git a/internal/api/authz/instance.go b/internal/api/authz/instance.go index 7ee8d605ca..0fe6d6c8aa 100644 --- a/internal/api/authz/instance.go +++ b/internal/api/authz/instance.go @@ -9,9 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/feature" ) -var ( - emptyInstance = &instance{} -) +var emptyInstance = &instance{} type Instance interface { InstanceID() string @@ -33,13 +31,13 @@ type InstanceVerifier interface { } type instance struct { - id string - domain string - projectID string - appID string - clientID string - orgID string - features feature.Features + id string + projectID string + appID string + clientID string + orgID string + defaultLanguage language.Tag + features feature.Features } func (i *instance) Block() *bool { @@ -67,7 +65,7 @@ func (i *instance) ConsoleApplicationID() string { } func (i *instance) DefaultLanguage() language.Tag { - return language.Und + return i.defaultLanguage } func (i *instance) DefaultOrganisationID() string { @@ -106,6 +104,16 @@ func WithInstanceID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, instanceKey, &instance{id: id}) } +func WithDefaultLanguage(ctx context.Context, defaultLanguage language.Tag) context.Context { + i, ok := ctx.Value(instanceKey).(*instance) + if !ok { + i = new(instance) + } + + i.defaultLanguage = defaultLanguage + return context.WithValue(ctx, instanceKey, i) +} + func WithConsole(ctx context.Context, projectID, appID string) context.Context { i, ok := ctx.Value(instanceKey).(*instance) if !ok { 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 index 0c5018dbb6..9fa568fb8b 100644 --- a/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go @@ -580,7 +580,7 @@ func TestServer_ExecutionTargetPreUserinfo(t *testing.T) { isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) - client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2) + client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, t, redirectURIImplicit, loginV2) require.NoError(t, err) type want struct { @@ -893,7 +893,7 @@ func TestServer_ExecutionTargetPreAccessToken(t *testing.T) { isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) - client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2) + client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, t, redirectURIImplicit, loginV2) require.NoError(t, err) type want struct { @@ -1255,10 +1255,9 @@ func createSAMLSP(t *testing.T, idpMetadata *saml.EntityDescriptor, binding stri } 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) + project := instance.CreateProject(ctx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), projectRoleCheck, hasProjectCheck) rootURL, sp := createSAMLSP(t, idpMetadata, binding) - _, err = instance.CreateSAMLClient(ctx, project.GetId(), sp) + _, err := instance.CreateSAMLClient(ctx, project.GetId(), sp) require.NoError(t, err) return project.GetId(), rootURL, sp } diff --git a/internal/api/grpc/action/v2beta/query.go b/internal/api/grpc/action/v2beta/query.go index 66bafa4e7d..1dbe80a8f7 100644 --- a/internal/api/grpc/action/v2beta/query.go +++ b/internal/api/grpc/action/v2beta/query.go @@ -164,7 +164,7 @@ func targetFieldNameToSortingColumn(field *action.TargetFieldName) query.Column } switch *field { case action.TargetFieldName_TARGET_FIELD_NAME_UNSPECIFIED: - return query.TargetColumnID + return query.TargetColumnCreationDate case action.TargetFieldName_TARGET_FIELD_NAME_ID: return query.TargetColumnID case action.TargetFieldName_TARGET_FIELD_NAME_CREATED_DATE: @@ -193,7 +193,7 @@ func executionFieldNameToSortingColumn(field *action.ExecutionFieldName) query.C } switch *field { case action.ExecutionFieldName_EXECUTION_FIELD_NAME_UNSPECIFIED: - return query.ExecutionColumnID + return query.ExecutionColumnCreationDate case action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID: return query.ExecutionColumnID case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CREATED_DATE: diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index 68b6053c2c..b5d36272d4 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -8,7 +8,9 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" authn_grpc "github.com/zitadel/zitadel/internal/api/grpc/authn" + "github.com/zitadel/zitadel/internal/api/grpc/org" text_grpc "github.com/zitadel/zitadel/internal/api/grpc/text" + user_converter "github.com/zitadel/zitadel/internal/api/grpc/user" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -65,7 +67,7 @@ func (s *Server) ExportData(ctx context.Context, req *admin_pb.ExportDataRequest /****************************************************************************************************************** Organization ******************************************************************************************************************/ - org := &admin_pb.DataOrg{OrgId: queriedOrg.ID, Org: &management_pb.AddOrgRequest{Name: queriedOrg.Name}} + org := &admin_pb.DataOrg{OrgId: queriedOrg.ID, OrgState: org.OrgStateToPb(queriedOrg.State), Org: &management_pb.AddOrgRequest{Name: queriedOrg.Name}} orgs[i] = org } @@ -567,6 +569,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w case domain.UserTypeHuman: dataUser := &v1_pb.DataHumanUser{ UserId: user.ID, + State: user_converter.UserStateToPb(user.State), User: &management_pb.ImportHumanUserRequest{ UserName: user.Username, Profile: &management_pb.ImportHumanUserRequest_Profile{ @@ -620,6 +623,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w case domain.UserTypeMachine: machineUsers = append(machineUsers, &v1_pb.DataMachineUser{ UserId: user.ID, + State: user_converter.UserStateToPb(user.State), User: &management_pb.AddMachineUserRequest{ UserName: user.Username, Name: user.Machine.Name, @@ -647,7 +651,6 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w ExpirationDate: timestamppb.New(key.Expiration), PublicKey: key.PublicKey, }) - } } @@ -736,7 +739,7 @@ func (s *Server) getProjectsAndApps(ctx context.Context, org string) ([]*v1_pb.D if err != nil { return nil, nil, nil, nil, nil, err } - queriedProjects, err := s.query.SearchProjects(ctx, &query.ProjectSearchQueries{Queries: []query.SearchQuery{projectSearch}}) + queriedProjects, err := s.query.SearchProjects(ctx, &query.ProjectSearchQueries{Queries: []query.SearchQuery{projectSearch}}, nil) if err != nil { return nil, nil, nil, nil, nil, err } @@ -763,7 +766,7 @@ func (s *Server) getProjectsAndApps(ctx context.Context, org string) ([]*v1_pb.D return nil, nil, nil, nil, nil, err } - queriedProjectRoles, err := s.query.SearchProjectRoles(ctx, false, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectRoleSearch}}) + queriedProjectRoles, err := s.query.SearchProjectRoles(ctx, false, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectRoleSearch}}, nil) if err != nil { return nil, nil, nil, nil, nil, err } @@ -888,7 +891,6 @@ func (s *Server) getNecessaryProjectGrantMembersForOrg(ctx context.Context, org break } } - } } } @@ -940,12 +942,11 @@ func (s *Server) getNecessaryOrgMembersForOrg(ctx context.Context, org string, p } func (s *Server) getNecessaryProjectGrantsForOrg(ctx context.Context, org string, processedOrgs []string, processedProjects []string) ([]*v1_pb.DataProjectGrant, error) { - projectGrantSearchOrg, err := query.NewProjectGrantResourceOwnerSearchQuery(org) if err != nil { return nil, err } - queriedProjectGrants, err := s.query.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectGrantSearchOrg}}) + queriedProjectGrants, err := s.query.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectGrantSearchOrg}}, nil) if err != nil { return nil, err } @@ -991,7 +992,7 @@ func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, p for _, userGrant := range queriedUserGrants.UserGrants { for _, projectID := range processedProjects { if projectID == userGrant.ProjectID { - //if usergrant is on a granted project + // if usergrant is on a granted project if userGrant.GrantID != "" { for _, grantID := range processedGrants { if grantID == userGrant.GrantID { @@ -1024,6 +1025,7 @@ func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, p } return userGrants, nil } + func (s *Server) getCustomLoginTexts(ctx context.Context, org string, languages []string) ([]*management_pb.SetCustomLoginTextsRequest, error) { customTexts := make([]*management_pb.SetCustomLoginTextsRequest, 0, len(languages)) for _, lang := range languages { diff --git a/internal/api/grpc/admin/idp_converter.go b/internal/api/grpc/admin/idp_converter.go index 3084031a73..fd7749d664 100644 --- a/internal/api/grpc/admin/idp_converter.go +++ b/internal/api/grpc/admin/idp_converter.go @@ -488,6 +488,7 @@ func addSAMLProviderToCommand(req *admin_pb.AddSAMLProviderRequest) *command.SAM WithSignedRequest: req.WithSignedRequest, NameIDFormat: nameIDFormat, TransientMappingAttributeName: req.GetTransientMappingAttributeName(), + FederatedLogoutEnabled: req.GetFederatedLogoutEnabled(), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } @@ -505,6 +506,7 @@ func updateSAMLProviderToCommand(req *admin_pb.UpdateSAMLProviderRequest) *comma WithSignedRequest: req.WithSignedRequest, NameIDFormat: nameIDFormat, TransientMappingAttributeName: req.GetTransientMappingAttributeName(), + FederatedLogoutEnabled: req.GetFederatedLogoutEnabled(), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 5bbcab27cf..84b0215f03 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -22,6 +22,7 @@ import ( action_grpc "github.com/zitadel/zitadel/internal/api/grpc/action" "github.com/zitadel/zitadel/internal/api/grpc/authn" "github.com/zitadel/zitadel/internal/api/grpc/management" + org_converter "github.com/zitadel/zitadel/internal/api/grpc/org" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -305,7 +306,8 @@ func importOrg1(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - _, err = s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), []string{}) + setOrgInactive := org_converter.OrgStateToDomain(org.OrgState) == domain.OrgStateInactive + _, err = s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), setOrgInactive, []string{}) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "org", Id: org.GetOrgId(), Message: err.Error()}) if _, err := s.query.OrgByID(ctx, true, org.OrgId); err != nil { @@ -474,7 +476,10 @@ func importHumanUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Import logging.Debugf("import user: %s", user.GetUserId()) human, passwordless, links := management.ImportHumanUserRequestToDomain(user.User) human.AggregateID = user.UserId - _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) + userState := user.State.ToDomain() + + //nolint:staticcheck + _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, &userState, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -510,7 +515,8 @@ func importMachineUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Impo } for _, user := range org.GetMachineUsers() { logging.Debugf("import user: %s", user.GetUserId()) - _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId())) + userState := user.State.ToDomain() + _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId()), &userState, nil) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "machine_user", Id: user.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -609,7 +615,6 @@ func importUserLinks(ctx context.Context, s *Server, errors *[]*admin_pb.ImportD successOrg.UserLinks = append(successOrg.UserLinks, &admin_pb.ImportDataSuccessUserLinks{UserId: userLinks.GetUserId(), IdpId: userLinks.GetIdpId(), ExternalUserId: userLinks.GetProvidedUserId(), DisplayName: userLinks.GetProvidedUserName()}) } return nil - } func importProjects(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) { @@ -621,7 +626,7 @@ func importProjects(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDa } for _, project := range org.GetProjects() { logging.Debugf("import project: %s", project.GetProjectId()) - _, err := s.command.AddProjectWithID(ctx, management.ProjectCreateToDomain(project.GetProject()), org.GetOrgId(), project.GetProjectId()) + _, err := s.command.AddProject(ctx, management.ProjectCreateToCommand(project.GetProject(), project.GetProjectId(), org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project", Id: project.GetProjectId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -750,6 +755,7 @@ func importActions(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDat } return nil } + func importProjectRoles(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -761,7 +767,7 @@ func importProjectRoles(ctx context.Context, s *Server, errors *[]*admin_pb.Impo logging.Debugf("import projectroles: %s", role.ProjectId+"_"+role.RoleKey) // TBD: why not command.BulkAddProjectRole? - _, err := s.command.AddProjectRole(ctx, management.AddProjectRoleRequestToDomain(role), org.GetOrgId()) + _, err := s.command.AddProjectRole(ctx, management.AddProjectRoleRequestToCommand(role, org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_role", Id: role.ProjectId + "_" + role.RoleKey, Message: err.Error()}) if isCtxTimeout(ctx) { @@ -805,6 +811,7 @@ func importResources(ctx context.Context, s *Server, errors *[]*admin_pb.ImportD importDomainClaimedMessageTexts(ctx, s, errors, org) importPasswordlessRegistrationMessageTexts(ctx, s, errors, org) importInviteUserMessageTexts(ctx, s, errors, org) + if err := importHumanUsers(ctx, s, errors, successOrg, org, count, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode); err != nil { return err } @@ -1023,7 +1030,7 @@ func importOrg2(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr if org.ProjectGrants != nil { for _, grant := range org.GetProjectGrants() { logging.Debugf("import projectgrant: %s", grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) - _, err := s.command.AddProjectGrantWithID(ctx, management.AddProjectGrantRequestToDomain(grant.GetProjectGrant()), grant.GetGrantId(), org.GetOrgId()) + _, err := s.command.AddProjectGrant(ctx, management.AddProjectGrantRequestToCommand(grant.GetProjectGrant(), grant.GetGrantId(), org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_grant", Id: org.GetOrgId() + "_" + grant.GetProjectGrant().GetProjectId() + "_" + grant.GetProjectGrant().GetGrantedOrgId(), Message: err.Error()}) if isCtxTimeout(ctx) { diff --git a/internal/api/grpc/admin/integration_test/import_test.go b/internal/api/grpc/admin/integration_test/import_test.go index 7d323e5ab8..4a546bbcf1 100644 --- a/internal/api/grpc/admin/integration_test/import_test.go +++ b/internal/api/grpc/admin/integration_test/import_test.go @@ -259,6 +259,12 @@ func TestServer_ImportData(t *testing.T) { Data: &admin.ImportDataRequest_DataOrgs{ DataOrgs: &admin.ImportDataOrg{ Orgs: []*admin.DataOrg{ + { + OrgId: orgIDs[4], + Org: &management.AddOrgRequest{ + Name: gofakeit.ProductName(), + }, + }, { OrgId: orgIDs[3], Org: &management.AddOrgRequest{ @@ -336,6 +342,9 @@ func TestServer_ImportData(t *testing.T) { }, Success: &admin.ImportDataSuccess{ Orgs: []*admin.ImportDataSuccessOrg{ + { + OrgId: orgIDs[4], + }, { OrgId: orgIDs[3], ProjectIds: projectIDs[2:4], @@ -363,6 +372,12 @@ func TestServer_ImportData(t *testing.T) { Data: &admin.ImportDataRequest_DataOrgs{ DataOrgs: &admin.ImportDataOrg{ Orgs: []*admin.DataOrg{ + { + OrgId: orgIDs[6], + Org: &management.AddOrgRequest{ + Name: gofakeit.ProductName(), + }, + }, { OrgId: orgIDs[5], Org: &management.AddOrgRequest{ @@ -383,6 +398,11 @@ func TestServer_ImportData(t *testing.T) { RoleKey: "role1", DisplayName: "role1", }, + { + ProjectId: projectIDs[4], + RoleKey: "role2", + DisplayName: "role2", + }, }, HumanUsers: []*v1.DataHumanUser{ { @@ -442,11 +462,15 @@ func TestServer_ImportData(t *testing.T) { }, Success: &admin.ImportDataSuccess{ Orgs: []*admin.ImportDataSuccessOrg{ + { + OrgId: orgIDs[6], + }, { OrgId: orgIDs[5], ProjectIds: projectIDs[4:5], ProjectRoles: []string{ projectIDs[4] + "_role1", + projectIDs[4] + "_role2", }, HumanUserIds: userIDs[2:3], ProjectGrants: []*admin.ImportDataSuccessProjectGrant{ diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index 293e7c74d7..ef97e47bb0 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -78,7 +78,7 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* if err != nil { return nil, err } - human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine + human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) // TODO: handle machine createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{ Name: req.Org.Name, CustomDomain: req.Org.Domain, @@ -93,8 +93,8 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* return nil, err } var userID string - if len(createdOrg.CreatedAdmins) == 1 { - userID = createdOrg.CreatedAdmins[0].ID + if len(createdOrg.OrgAdmins) == 1 { + userID = createdOrg.OrgAdmins[0].GetID() } return &admin_pb.SetUpOrgResponse{ Details: object.DomainToAddDetailsPb(createdOrg.ObjectDetails), diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go index baa45c6c6e..e146ac2db6 100644 --- a/internal/api/grpc/feature/v2/converter.go +++ b/internal/api/grpc/feature/v2/converter.go @@ -172,7 +172,7 @@ func improvedPerformanceTypesToPb(types []feature.ImprovedPerformanceType) []fea func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb.ImprovedPerformance { switch typ { - case feature.ImprovedPerformanceTypeUnknown: + case feature.ImprovedPerformanceTypeUnspecified: return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED case feature.ImprovedPerformanceTypeOrgByID: return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID @@ -205,7 +205,7 @@ func improvedPerformanceListToDomain(list []feature_pb.ImprovedPerformance) []fe func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.ImprovedPerformanceType { switch typ { case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED: - return feature.ImprovedPerformanceTypeUnknown + return feature.ImprovedPerformanceTypeUnspecified case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID: return feature.ImprovedPerformanceTypeOrgByID case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT: @@ -217,6 +217,6 @@ func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.Imp case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED: return feature.ImprovedPerformanceTypeOrgDomainVerified default: - return feature.ImprovedPerformanceTypeUnknown + return feature.ImprovedPerformanceTypeUnspecified } } diff --git a/internal/api/grpc/feature/v2beta/converter.go b/internal/api/grpc/feature/v2beta/converter.go index bbb375716e..9739e1c4c8 100644 --- a/internal/api/grpc/feature/v2beta/converter.go +++ b/internal/api/grpc/feature/v2beta/converter.go @@ -109,7 +109,7 @@ func improvedPerformanceTypesToPb(types []feature.ImprovedPerformanceType) []fea func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb.ImprovedPerformance { switch typ { - case feature.ImprovedPerformanceTypeUnknown: + case feature.ImprovedPerformanceTypeUnspecified: return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED case feature.ImprovedPerformanceTypeOrgByID: return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID @@ -142,7 +142,7 @@ func improvedPerformanceListToDomain(list []feature_pb.ImprovedPerformance) []fe func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.ImprovedPerformanceType { switch typ { case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED: - return feature.ImprovedPerformanceTypeUnknown + return feature.ImprovedPerformanceTypeUnspecified case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID: return feature.ImprovedPerformanceTypeOrgByID case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT: @@ -154,6 +154,6 @@ func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.Imp case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED: return feature.ImprovedPerformanceTypeOrgDomainVerified default: - return feature.ImprovedPerformanceTypeUnknown + return feature.ImprovedPerformanceTypeUnspecified } } diff --git a/internal/api/grpc/filter/v2/converter.go b/internal/api/grpc/filter/v2/converter.go new file mode 100644 index 0000000000..7a7d7cd8d7 --- /dev/null +++ b/internal/api/grpc/filter/v2/converter.go @@ -0,0 +1,50 @@ +package filter + +import ( + "fmt" + + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" +) + +func TimestampMethodPbToQuery(method filter.TimestampFilterMethod) query.TimestampComparison { + switch method { + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_EQUALS: + return query.TimestampEquals + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE: + return query.TimestampLess + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER: + return query.TimestampGreater + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS: + return query.TimestampLessOrEquals + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS: + return query.TimestampGreaterOrEquals + 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 6269f122c0..caab5121b8 100644 --- a/internal/api/grpc/idp/converter.go +++ b/internal/api/grpc/idp/converter.go @@ -669,6 +669,7 @@ func samlConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.SAMLI WithSignedRequest: template.WithSignedRequest, NameIdFormat: nameIDFormat, TransientMappingAttributeName: gu.Ptr(template.TransientMappingAttributeName), + FederatedLogoutEnabled: gu.Ptr(template.FederatedLogoutEnabled), }, } } diff --git a/internal/api/grpc/idp/v2/query.go b/internal/api/grpc/idp/v2/query.go index de78775804..082a94d18f 100644 --- a/internal/api/grpc/idp/v2/query.go +++ b/internal/api/grpc/idp/v2/query.go @@ -336,6 +336,7 @@ func samlConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.SAMLIDPTemplate WithSignedRequest: template.WithSignedRequest, NameIdFormat: nameIDFormat, TransientMappingAttributeName: gu.Ptr(template.TransientMappingAttributeName), + FederatedLogoutEnabled: gu.Ptr(template.FederatedLogoutEnabled), }, } } diff --git a/internal/api/grpc/instance/converter.go b/internal/api/grpc/instance/converter.go index 4094da4a77..b894a064ff 100644 --- a/internal/api/grpc/instance/converter.go +++ b/internal/api/grpc/instance/converter.go @@ -28,7 +28,7 @@ func InstanceToPb(instance *query.Instance) *instance_pb.Instance { Name: instance.Name, Domains: DomainsToPb(instance.Domains), Version: build.Version(), - State: instance_pb.State_STATE_RUNNING, //TODO: change when delete is implemented + State: instance_pb.State_STATE_RUNNING, // TODO: change when delete is implemented } } @@ -44,7 +44,7 @@ func InstanceDetailToPb(instance *query.Instance) *instance_pb.InstanceDetail { Name: instance.Name, Domains: DomainsToPb(instance.Domains), Version: build.Version(), - State: instance_pb.State_STATE_RUNNING, //TODO: change when delete is implemented + State: instance_pb.State_STATE_RUNNING, // TODO: change when delete is implemented } } diff --git a/internal/api/grpc/instance/v2beta/converter.go b/internal/api/grpc/instance/v2beta/converter.go new file mode 100644 index 0000000000..8bff682606 --- /dev/null +++ b/internal/api/grpc/instance/v2beta/converter.go @@ -0,0 +1,246 @@ +package instance + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/cmd/build" + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func InstancesToPb(instances []*query.Instance) []*instance.Instance { + list := []*instance.Instance{} + for _, instance := range instances { + list = append(list, ToProtoObject(instance)) + } + return list +} + +func ToProtoObject(inst *query.Instance) *instance.Instance { + return &instance.Instance{ + Id: inst.ID, + Name: inst.Name, + Domains: DomainsToPb(inst.Domains), + Version: build.Version(), + ChangeDate: timestamppb.New(inst.ChangeDate), + CreationDate: timestamppb.New(inst.CreationDate), + } +} + +func DomainsToPb(domains []*query.InstanceDomain) []*instance.Domain { + d := []*instance.Domain{} + for _, dm := range domains { + pbDomain := DomainToPb(dm) + d = append(d, pbDomain) + } + return d +} + +func DomainToPb(d *query.InstanceDomain) *instance.Domain { + return &instance.Domain{ + Domain: d.Domain, + Primary: d.IsPrimary, + Generated: d.IsGenerated, + InstanceId: d.InstanceID, + CreationDate: timestamppb.New(d.CreationDate), + } +} + +func ListInstancesRequestToModel(req *instance.ListInstancesRequest, sysDefaults systemdefaults.SystemDefaults) (*query.InstanceSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(sysDefaults, req.GetPagination()) + if err != nil { + return nil, err + } + + queries, err := instanceQueriesToModel(req.GetQueries()) + if err != nil { + return nil, err + } + + return &query.InstanceSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToInstanceColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil + +} + +func fieldNameToInstanceColumn(fieldName instance.FieldName) query.Column { + switch fieldName { + case instance.FieldName_FIELD_NAME_ID: + return query.InstanceColumnID + case instance.FieldName_FIELD_NAME_NAME: + return query.InstanceColumnName + case instance.FieldName_FIELD_NAME_CREATION_DATE: + return query.InstanceColumnCreationDate + case instance.FieldName_FIELD_NAME_UNSPECIFIED: + fallthrough + default: + return query.Column{} + } +} + +func instanceQueriesToModel(queries []*instance.Query) (_ []query.SearchQuery, err error) { + q := []query.SearchQuery{} + for _, query := range queries { + model, err := instanceQueryToModel(query) + if err != nil { + return nil, err + } + q = append(q, model) + } + return q, nil +} + +func instanceQueryToModel(searchQuery *instance.Query) (query.SearchQuery, error) { + switch q := searchQuery.GetQuery().(type) { + case *instance.Query_IdQuery: + return query.NewInstanceIDsListSearchQuery(q.IdQuery.GetIds()...) + case *instance.Query_DomainQuery: + return query.NewInstanceDomainsListSearchQuery(q.DomainQuery.GetDomains()...) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "INST-3m0se", "List.Query.Invalid") + } +} + +func ListCustomDomainsRequestToModel(req *instance.ListCustomDomainsRequest, defaults systemdefaults.SystemDefaults) (*query.InstanceDomainSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(defaults, req.GetPagination()) + if err != nil { + return nil, err + } + + queries, err := domainQueriesToModel(req.GetQueries()) + if err != nil { + return nil, err + } + + return &query.InstanceDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToInstanceDomainColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func fieldNameToInstanceDomainColumn(fieldName instance.DomainFieldName) query.Column { + switch fieldName { + case instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN: + return query.InstanceDomainDomainCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED: + return query.InstanceDomainIsGeneratedCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY: + return query.InstanceDomainIsPrimaryCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE: + return query.InstanceDomainCreationDateCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED: + fallthrough + default: + return query.Column{} + } +} + +func domainQueriesToModel(queries []*instance.DomainSearchQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = domainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func domainQueryToModel(searchQuery *instance.DomainSearchQuery) (query.SearchQuery, error) { + switch q := searchQuery.GetQuery().(type) { + case *instance.DomainSearchQuery_DomainQuery: + return query.NewInstanceDomainDomainSearchQuery(object.TextMethodToQuery(q.DomainQuery.GetMethod()), q.DomainQuery.GetDomain()) + case *instance.DomainSearchQuery_GeneratedQuery: + return query.NewInstanceDomainGeneratedSearchQuery(q.GeneratedQuery.GetGenerated()) + case *instance.DomainSearchQuery_PrimaryQuery: + return query.NewInstanceDomainPrimarySearchQuery(q.PrimaryQuery.GetPrimary()) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid") + } +} + +func ListTrustedDomainsRequestToModel(req *instance.ListTrustedDomainsRequest, defaults systemdefaults.SystemDefaults) (*query.InstanceTrustedDomainSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(defaults, req.GetPagination()) + if err != nil { + return nil, err + } + + queries, err := trustedDomainQueriesToModel(req.GetQueries()) + if err != nil { + return nil, err + } + + return &query.InstanceTrustedDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToInstanceTrustedDomainColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func trustedDomainQueriesToModel(queries []*instance.TrustedDomainSearchQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = trustedDomainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func trustedDomainQueryToModel(searchQuery *instance.TrustedDomainSearchQuery) (query.SearchQuery, error) { + switch q := searchQuery.GetQuery().(type) { + case *instance.TrustedDomainSearchQuery_DomainQuery: + return query.NewInstanceTrustedDomainDomainSearchQuery(object.TextMethodToQuery(q.DomainQuery.GetMethod()), q.DomainQuery.GetDomain()) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid") + } +} + +func trustedDomainsToPb(domains []*query.InstanceTrustedDomain) []*instance.TrustedDomain { + d := make([]*instance.TrustedDomain, len(domains)) + for i, domain := range domains { + d[i] = trustedDomainToPb(domain) + } + return d +} + +func trustedDomainToPb(d *query.InstanceTrustedDomain) *instance.TrustedDomain { + return &instance.TrustedDomain{ + Domain: d.Domain, + InstanceId: d.InstanceID, + CreationDate: timestamppb.New(d.CreationDate), + } +} + +func fieldNameToInstanceTrustedDomainColumn(fieldName instance.TrustedDomainFieldName) query.Column { + switch fieldName { + case instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN: + return query.InstanceTrustedDomainDomainCol + case instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE: + return query.InstanceTrustedDomainCreationDateCol + case instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED: + fallthrough + default: + return query.Column{} + } +} diff --git a/internal/api/grpc/instance/v2beta/converter_test.go b/internal/api/grpc/instance/v2beta/converter_test.go new file mode 100644 index 0000000000..150678010c --- /dev/null +++ b/internal/api/grpc/instance/v2beta/converter_test.go @@ -0,0 +1,390 @@ +package instance + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/cmd/build" + "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" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" +) + +func Test_InstancesToPb(t *testing.T) { + instances := []*query.Instance{ + { + ID: "instance1", + Name: "Instance One", + Domains: []*query.InstanceDomain{ + { + Domain: "example.com", + IsPrimary: true, + IsGenerated: false, + Sequence: 1, + CreationDate: time.Unix(123, 0), + ChangeDate: time.Unix(124, 0), + InstanceID: "instance1", + }, + }, + Sequence: 1, + CreationDate: time.Unix(123, 0), + ChangeDate: time.Unix(124, 0), + }, + } + + want := []*instance.Instance{ + { + Id: "instance1", + Name: "Instance One", + Domains: []*instance.Domain{ + { + Domain: "example.com", + Primary: true, + Generated: false, + InstanceId: "instance1", + CreationDate: ×tamppb.Timestamp{Seconds: 123}, + }, + }, + Version: build.Version(), + ChangeDate: ×tamppb.Timestamp{Seconds: 124}, + CreationDate: ×tamppb.Timestamp{Seconds: 123}, + }, + } + + got := InstancesToPb(instances) + assert.Equal(t, want, got) +} + +func Test_ListInstancesRequestToModel(t *testing.T) { + t.Parallel() + + searchInstanceByID, err := query.NewInstanceIDsListSearchQuery("instance1", "instance2") + require.Nil(t, err) + + tt := []struct { + testName string + inputRequest *instance.ListInstancesRequest + maxQueryLimit uint64 + expectedResult *query.InstanceSearchQueries + expectedError error + }{ + { + testName: "when query limit exceeds max query limit should return invalid argument error", + maxQueryLimit: 1, + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.FieldName_FIELD_NAME_ID.Enum(), + Queries: []*instance.Query{{Query: &instance.Query_IdQuery{IdQuery: &instance.IdsQuery{Ids: []string{"instance1", "instance2"}}}}}, + }, + expectedError: zerrors.ThrowInvalidArgumentf(errors.New("given: 10, allowed: 1"), "QUERY-4M0fs", "Errors.Query.LimitExceeded"), + }, + { + testName: "when valid request should return instance search query model", + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.FieldName_FIELD_NAME_ID.Enum(), + Queries: []*instance.Query{{Query: &instance.Query_IdQuery{IdQuery: &instance.IdsQuery{Ids: []string{"instance1", "instance2"}}}}}, + }, + expectedResult: &query.InstanceSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 10, + Asc: true, + SortingColumn: query.InstanceColumnID, + }, + Queries: []query.SearchQuery{searchInstanceByID}, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + sysDefaults := systemdefaults.SystemDefaults{MaxQueryLimit: tc.maxQueryLimit} + + got, err := ListInstancesRequestToModel(tc.inputRequest, sysDefaults) + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResult, got) + + }) + } +} + +func Test_fieldNameToInstanceColumn(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + fieldName instance.FieldName + want query.Column + }{ + { + name: "ID field", + fieldName: instance.FieldName_FIELD_NAME_ID, + want: query.InstanceColumnID, + }, + { + name: "Name field", + fieldName: instance.FieldName_FIELD_NAME_NAME, + want: query.InstanceColumnName, + }, + { + name: "Creation Date field", + fieldName: instance.FieldName_FIELD_NAME_CREATION_DATE, + want: query.InstanceColumnCreationDate, + }, + { + name: "Unknown field", + fieldName: instance.FieldName(99), + want: query.Column{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := fieldNameToInstanceColumn(tt.fieldName) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_instanceQueryToModel(t *testing.T) { + t.Parallel() + + searchInstanceByID, err := query.NewInstanceIDsListSearchQuery("instance1") + require.Nil(t, err) + + searchInstanceByDomain, err := query.NewInstanceDomainsListSearchQuery("example.com") + require.Nil(t, err) + + tests := []struct { + name string + searchQuery *instance.Query + want query.SearchQuery + wantErr bool + }{ + { + name: "ID Query", + searchQuery: &instance.Query{ + Query: &instance.Query_IdQuery{ + IdQuery: &instance.IdsQuery{ + Ids: []string{"instance1"}, + }, + }, + }, + want: searchInstanceByID, + wantErr: false, + }, + { + name: "Domain Query", + searchQuery: &instance.Query{ + Query: &instance.Query_DomainQuery{ + DomainQuery: &instance.DomainsQuery{ + Domains: []string{"example.com"}, + }, + }, + }, + want: searchInstanceByDomain, + wantErr: false, + }, + { + name: "Invalid Query", + searchQuery: &instance.Query{ + Query: nil, + }, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := instanceQueryToModel(tt.searchQuery) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func Test_ListCustomDomainsRequestToModel(t *testing.T) { + t.Parallel() + + querySearchRes, err := query.NewInstanceDomainDomainSearchQuery(query.TextEquals, "example.com") + require.Nil(t, err) + + queryGeneratedRes, err := query.NewInstanceDomainGeneratedSearchQuery(false) + require.Nil(t, err) + + tests := []struct { + name string + inputRequest *instance.ListCustomDomainsRequest + maxQueryLimit uint64 + expectedResult *query.InstanceDomainSearchQueries + expectedError error + }{ + { + name: "when query limit exceeds max query limit should return invalid argument error", + inputRequest: &instance.ListCustomDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN, + Queries: []*instance.DomainSearchQuery{ + { + Query: &instance.DomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{ + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + Domain: "example.com", + }, + }, + }, + }, + }, + maxQueryLimit: 1, + expectedError: zerrors.ThrowInvalidArgumentf(errors.New("given: 10, allowed: 1"), "QUERY-4M0fs", "Errors.Query.LimitExceeded"), + }, + { + name: "when valid request should return domain search query model", + inputRequest: &instance.ListCustomDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY, + Queries: []*instance.DomainSearchQuery{ + { + Query: &instance.DomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, Domain: "example.com"}}, + }, + { + Query: &instance.DomainSearchQuery_GeneratedQuery{ + GeneratedQuery: &instance.DomainGeneratedQuery{Generated: false}}, + }, + }, + }, + maxQueryLimit: 100, + expectedResult: &query.InstanceDomainSearchQueries{ + SearchRequest: query.SearchRequest{Offset: 0, Limit: 10, Asc: true, SortingColumn: query.InstanceDomainIsPrimaryCol}, + Queries: []query.SearchQuery{ + querySearchRes, + queryGeneratedRes, + }, + }, + expectedError: nil, + }, + { + name: "when invalid query should return error", + inputRequest: &instance.ListCustomDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED, + Queries: []*instance.DomainSearchQuery{ + { + Query: nil, + }, + }, + }, + maxQueryLimit: 100, + expectedResult: nil, + expectedError: zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + sysDefaults := systemdefaults.SystemDefaults{MaxQueryLimit: tt.maxQueryLimit} + + got, err := ListCustomDomainsRequestToModel(tt.inputRequest, sysDefaults) + assert.Equal(t, tt.expectedError, err) + assert.Equal(t, tt.expectedResult, got) + }) + } +} + +func Test_ListTrustedDomainsRequestToModel(t *testing.T) { + t.Parallel() + + querySearchRes, err := query.NewInstanceTrustedDomainDomainSearchQuery(query.TextEquals, "example.com") + require.Nil(t, err) + + tests := []struct { + name string + inputRequest *instance.ListTrustedDomainsRequest + maxQueryLimit uint64 + expectedResult *query.InstanceTrustedDomainSearchQueries + expectedError error + }{ + { + name: "when query limit exceeds max query limit should return invalid argument error", + inputRequest: &instance.ListTrustedDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN, + Queries: []*instance.TrustedDomainSearchQuery{ + { + Query: &instance.TrustedDomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{ + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + Domain: "example.com", + }, + }, + }, + }, + }, + maxQueryLimit: 1, + expectedError: zerrors.ThrowInvalidArgumentf(errors.New("given: 10, allowed: 1"), "QUERY-4M0fs", "Errors.Query.LimitExceeded"), + }, + { + name: "when valid request should return domain search query model", + inputRequest: &instance.ListTrustedDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE, + Queries: []*instance.TrustedDomainSearchQuery{ + { + Query: &instance.TrustedDomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, Domain: "example.com"}}, + }, + }, + }, + maxQueryLimit: 100, + expectedResult: &query.InstanceTrustedDomainSearchQueries{ + SearchRequest: query.SearchRequest{Offset: 0, Limit: 10, Asc: true, SortingColumn: query.InstanceTrustedDomainCreationDateCol}, + Queries: []query.SearchQuery{querySearchRes}, + }, + expectedError: nil, + }, + { + name: "when invalid query should return error", + inputRequest: &instance.ListTrustedDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + Queries: []*instance.TrustedDomainSearchQuery{ + { + Query: nil, + }, + }, + }, + maxQueryLimit: 100, + expectedResult: nil, + expectedError: zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + sysDefaults := systemdefaults.SystemDefaults{MaxQueryLimit: tt.maxQueryLimit} + + got, err := ListTrustedDomainsRequestToModel(tt.inputRequest, sysDefaults) + assert.Equal(t, tt.expectedError, err) + assert.Equal(t, tt.expectedResult, got) + }) + } +} diff --git a/internal/api/grpc/instance/v2beta/domain.go b/internal/api/grpc/instance/v2beta/domain.go new file mode 100644 index 0000000000..439c6e5d8d --- /dev/null +++ b/internal/api/grpc/instance/v2beta/domain.go @@ -0,0 +1,50 @@ +package instance + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func (s *Server) AddCustomDomain(ctx context.Context, req *instance.AddCustomDomainRequest) (*instance.AddCustomDomainResponse, error) { + details, err := s.command.AddInstanceDomain(ctx, req.GetDomain()) + if err != nil { + return nil, err + } + return &instance.AddCustomDomainResponse{ + CreationDate: timestamppb.New(details.CreationDate), + }, nil +} + +func (s *Server) RemoveCustomDomain(ctx context.Context, req *instance.RemoveCustomDomainRequest) (*instance.RemoveCustomDomainResponse, error) { + details, err := s.command.RemoveInstanceDomain(ctx, req.GetDomain()) + if err != nil { + return nil, err + } + return &instance.RemoveCustomDomainResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) AddTrustedDomain(ctx context.Context, req *instance.AddTrustedDomainRequest) (*instance.AddTrustedDomainResponse, error) { + details, err := s.command.AddTrustedDomain(ctx, req.GetDomain()) + if err != nil { + return nil, err + } + return &instance.AddTrustedDomainResponse{ + CreationDate: timestamppb.New(details.CreationDate), + }, nil +} + +func (s *Server) RemoveTrustedDomain(ctx context.Context, req *instance.RemoveTrustedDomainRequest) (*instance.RemoveTrustedDomainResponse, error) { + details, err := s.command.RemoveTrustedDomain(ctx, req.GetDomain()) + if err != nil { + return nil, err + } + + return &instance.RemoveTrustedDomainResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, nil +} diff --git a/internal/api/grpc/instance/v2beta/instance.go b/internal/api/grpc/instance/v2beta/instance.go new file mode 100644 index 0000000000..b1c36e74bb --- /dev/null +++ b/internal/api/grpc/instance/v2beta/instance.go @@ -0,0 +1,32 @@ +package instance + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func (s *Server) DeleteInstance(ctx context.Context, request *instance.DeleteInstanceRequest) (*instance.DeleteInstanceResponse, error) { + obj, err := s.command.RemoveInstance(ctx, request.GetInstanceId()) + if err != nil { + return nil, err + } + + return &instance.DeleteInstanceResponse{ + DeletionDate: timestamppb.New(obj.EventDate), + }, nil + +} + +func (s *Server) UpdateInstance(ctx context.Context, request *instance.UpdateInstanceRequest) (*instance.UpdateInstanceResponse, error) { + obj, err := s.command.UpdateInstance(ctx, request.GetInstanceName()) + if err != nil { + return nil, err + } + + return &instance.UpdateInstanceResponse{ + ChangeDate: timestamppb.New(obj.EventDate), + }, nil +} diff --git a/internal/api/grpc/instance/v2beta/integration_test/domain_test.go b/internal/api/grpc/instance/v2beta/integration_test/domain_test.go new file mode 100644 index 0000000000..a0e2011cfc --- /dev/null +++ b/internal/api/grpc/instance/v2beta/integration_test/domain_test.go @@ -0,0 +1,350 @@ +//go:build integration + +package instance_test + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/internal/integration" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func TestAddCustomDomain(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + + inst := integration.NewInstance(ctxWithSysAuthZ) + iamOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeIAMOwner) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + inputRequest *instance.AddCustomDomainRequest + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.AddCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: gofakeit.DomainName(), + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.AddCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: gofakeit.DomainName(), + }, + inputContext: iamOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when invalid domain should return invalid argument error", + inputRequest: &instance.AddCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: " ", + }, + inputContext: ctxWithSysAuthZ, + expectedErrorCode: codes.InvalidArgument, + expectedErrorMsg: "Errors.Invalid.Argument (INST-28nlD)", + }, + { + testName: "when valid request should return successful response", + inputRequest: &instance.AddCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: " " + gofakeit.DomainName(), + }, + inputContext: ctxWithSysAuthZ, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Cleanup(func() { + if tc.expectedErrorMsg == "" { + inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{Domain: strings.TrimSpace(tc.inputRequest.Domain)}) + } + }) + + // Test + res, err := inst.Client.InstanceV2Beta.AddCustomDomain(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + assert.NotNil(t, res) + assert.NotEmpty(t, res.GetCreationDate()) + } + }) + } +} + +func TestRemoveCustomDomain(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + iamOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeIAMOwner) + + customDomain := gofakeit.DomainName() + + _, err := inst.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), Domain: customDomain}) + require.Nil(t, err) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), Domain: customDomain}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + inputRequest *instance.RemoveCustomDomainRequest + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.RemoveCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: "custom1", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.RemoveCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: "custom1", + }, + inputContext: iamOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when invalid domain should return invalid argument error", + inputRequest: &instance.RemoveCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: " ", + }, + inputContext: ctxWithSysAuthZ, + expectedErrorCode: codes.InvalidArgument, + expectedErrorMsg: "Errors.Invalid.Argument (INST-39nls)", + }, + { + testName: "when valid request should return successful response", + inputRequest: &instance.RemoveCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: " " + customDomain, + }, + inputContext: ctxWithSysAuthZ, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.RemoveCustomDomain(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + assert.NotNil(t, res) + assert.NotEmpty(t, res.GetDeletionDate()) + } + }) + } +} + +func TestAddTrustedDomain(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + inputRequest *instance.AddTrustedDomainRequest + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.AddTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: "trusted1", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.AddTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: "trusted1", + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when invalid domain should return invalid argument error", + inputRequest: &instance.AddTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: " ", + }, + inputContext: ctxWithSysAuthZ, + expectedErrorCode: codes.InvalidArgument, + expectedErrorMsg: "Errors.Invalid.Argument (COMMA-Stk21)", + }, + { + testName: "when valid request should return successful response", + inputRequest: &instance.AddTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: " " + gofakeit.DomainName(), + }, + inputContext: ctxWithSysAuthZ, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Cleanup(func() { + if tc.expectedErrorMsg == "" { + inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{Domain: strings.TrimSpace(tc.inputRequest.Domain)}) + } + }) + + // Test + res, err := inst.Client.InstanceV2Beta.AddTrustedDomain(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + assert.NotNil(t, res) + assert.NotEmpty(t, res.GetCreationDate()) + } + }) + } +} + +func TestRemoveTrustedDomain(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + trustedDomain := gofakeit.DomainName() + + _, err := inst.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), Domain: trustedDomain}) + require.Nil(t, err) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), Domain: trustedDomain}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + inputRequest *instance.RemoveTrustedDomainRequest + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.RemoveTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: "trusted1", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.RemoveTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: "trusted1", + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when valid request should return successful response", + inputRequest: &instance.RemoveTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: " " + trustedDomain, + }, + inputContext: ctxWithSysAuthZ, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.RemoveTrustedDomain(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NotNil(t, res) + require.NotEmpty(t, res.GetDeletionDate()) + } + }) + } +} diff --git a/internal/api/grpc/instance/v2beta/integration_test/instance_test.go b/internal/api/grpc/instance/v2beta/integration_test/instance_test.go new file mode 100644 index 0000000000..ae277c6d13 --- /dev/null +++ b/internal/api/grpc/instance/v2beta/integration_test/instance_test.go @@ -0,0 +1,163 @@ +//go:build integration + +package instance_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/internal/integration" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func TestDeleteInstace(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + + inst := integration.NewInstance(ctxWithSysAuthZ) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputRequest *instance.DeleteInstanceRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedInstanceID string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.DeleteInstanceRequest{ + InstanceId: " ", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when invalid input should return invalid argument error", + inputRequest: &instance.DeleteInstanceRequest{ + InstanceId: inst.ID() + "invalid", + }, + inputContext: ctxWithSysAuthZ, + expectedErrorCode: codes.NotFound, + expectedErrorMsg: "Instance not found (COMMA-AE3GS)", + }, + { + testName: "when delete succeeds should return deletion date", + inputRequest: &instance.DeleteInstanceRequest{ + InstanceId: inst.ID(), + }, + inputContext: ctxWithSysAuthZ, + expectedInstanceID: inst.ID(), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.DeleteInstance(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + if tc.expectedErrorMsg == "" { + require.NotNil(t, res) + require.NotEmpty(t, res.GetDeletionDate()) + } + }) + } +} + +func TestUpdateInstace(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + + inst := integration.NewInstance(ctxWithSysAuthZ) + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputRequest *instance.UpdateInstanceRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedNewName string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.UpdateInstanceRequest{ + InstanceId: inst.ID(), + InstanceName: " ", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.UpdateInstanceRequest{ + InstanceId: inst.ID(), + InstanceName: " ", + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when update succeeds should change instance name", + inputRequest: &instance.UpdateInstanceRequest{ + InstanceId: inst.ID(), + InstanceName: "an-updated-name", + }, + inputContext: ctxWithSysAuthZ, + expectedNewName: "an-updated-name", + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.UpdateInstance(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + if tc.expectedErrorMsg == "" { + + require.NotNil(t, res) + assert.NotEmpty(t, res.GetChangeDate()) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, 20*time.Second) + require.EventuallyWithT(t, func(tt *assert.CollectT) { + retrievedInstance, err := inst.Client.InstanceV2Beta.GetInstance(tc.inputContext, &instance.GetInstanceRequest{InstanceId: inst.ID()}) + require.Nil(tt, err) + assert.Equal(tt, tc.expectedNewName, retrievedInstance.GetInstance().GetName()) + }, retryDuration, tick, "timeout waiting for expected execution result") + } + }) + } +} diff --git a/internal/api/grpc/instance/v2beta/integration_test/query_test.go b/internal/api/grpc/instance/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..e59a16a932 --- /dev/null +++ b/internal/api/grpc/instance/v2beta/integration_test/query_test.go @@ -0,0 +1,370 @@ +//go:build integration + +package instance_test + +import ( + "context" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/internal/integration" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" +) + +func TestGetInstance(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + expectedInstanceID string + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when unauthN context should return unauthN error", + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when request succeeds should return matching instance", + inputContext: ctxWithSysAuthZ, + expectedInstanceID: inst.ID(), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.GetInstance(tc.inputContext, &instance.GetInstanceRequest{InstanceId: inst.ID()}) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NoError(t, err) + assert.Equal(t, tc.expectedInstanceID, res.GetInstance().GetId()) + } + }) + } +} + +func TestListInstances(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + + instances := make([]*integration.Instance, 2) + inst := integration.NewInstance(ctxWithSysAuthZ) + inst2 := integration.NewInstance(ctxWithSysAuthZ) + instances[0], instances[1] = inst, inst2 + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst2.ID()}) + }) + + // Sort in descending order + slices.SortFunc(instances, func(i1, i2 *integration.Instance) int { + res := i1.Instance.Details.CreationDate.AsTime().Compare(i2.Instance.Details.CreationDate.AsTime()) + if res == 0 { + return res + } + return -res + }) + + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + tt := []struct { + testName string + inputRequest *instance.ListInstancesRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedInstances []string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when valid request with filter should return paginated response", + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + SortingColumn: instance.FieldName_FIELD_NAME_CREATION_DATE.Enum(), + Queries: []*instance.Query{ + { + Query: &instance.Query_IdQuery{ + IdQuery: &instance.IdsQuery{ + Ids: []string{inst.ID(), inst2.ID()}, + }, + }, + }, + }, + }, + inputContext: ctxWithSysAuthZ, + expectedInstances: []string{inst2.ID(), inst.ID()}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.ListInstances(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NotNil(t, res) + + require.Len(t, res.GetInstances(), len(tc.expectedInstances)) + + for i, ins := range res.GetInstances() { + assert.Equal(t, tc.expectedInstances[i], ins.GetId()) + } + } + }) + } +} + +func TestListCustomDomains(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + d1, d2 := "custom."+gofakeit.DomainName(), "custom."+gofakeit.DomainName() + + _, err := inst.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), Domain: d1}) + require.Nil(t, err) + _, err = inst.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), Domain: d2}) + require.Nil(t, err) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), Domain: d1}) + inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), Domain: d2}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputRequest *instance.ListCustomDomainsRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedDomains []string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.ListCustomDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing"}, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.ListCustomDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE, + Queries: []*instance.DomainSearchQuery{ + { + Query: &instance.DomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS}, + }, + }, + }, + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when valid request with filter should return paginated response", + inputRequest: &instance.ListCustomDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE, + Queries: []*instance.DomainSearchQuery{ + { + Query: &instance.DomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS}, + }, + }, + }, + }, + inputContext: ctxWithSysAuthZ, + expectedDomains: []string{d1, d2}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.ListCustomDomains(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + domains := []string{} + for _, d := range res.GetDomains() { + domains = append(domains, d.GetDomain()) + } + + assert.Subset(t, domains, tc.expectedDomains) + } + }) + } +} + +func TestListTrustedDomains(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + d1, d2 := "trusted."+gofakeit.DomainName(), "trusted."+gofakeit.DomainName() + + _, err := inst.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), Domain: d1}) + require.Nil(t, err) + _, err = inst.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), Domain: d2}) + require.Nil(t, err) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), Domain: d1}) + inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), Domain: d2}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputRequest *instance.ListTrustedDomainsRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedDomains []string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.ListTrustedDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.ListTrustedDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when valid request with filter should return paginated response", + inputRequest: &instance.ListTrustedDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE, + Queries: []*instance.TrustedDomainSearchQuery{ + { + Query: &instance.TrustedDomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Domain: "trusted", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS}, + }, + }, + }, + }, + inputContext: ctxWithSysAuthZ, + expectedDomains: []string{d1, d2}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.ListTrustedDomains(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NotNil(t, res) + + domains := []string{} + for _, d := range res.GetTrustedDomain() { + domains = append(domains, d.GetDomain()) + } + + assert.Subset(t, domains, tc.expectedDomains) + } + }) + } +} diff --git a/internal/api/grpc/instance/v2beta/query.go b/internal/api/grpc/instance/v2beta/query.go new file mode 100644 index 0000000000..74f79313ea --- /dev/null +++ b/internal/api/grpc/instance/v2beta/query.go @@ -0,0 +1,70 @@ +package instance + +import ( + "context" + + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func (s *Server) GetInstance(ctx context.Context, _ *instance.GetInstanceRequest) (*instance.GetInstanceResponse, error) { + inst, err := s.query.Instance(ctx, true) + if err != nil { + return nil, err + } + + return &instance.GetInstanceResponse{ + Instance: ToProtoObject(inst), + }, nil +} + +func (s *Server) ListInstances(ctx context.Context, req *instance.ListInstancesRequest) (*instance.ListInstancesResponse, error) { + queries, err := ListInstancesRequestToModel(req, s.systemDefaults) + if err != nil { + return nil, err + } + + instances, err := s.query.SearchInstances(ctx, queries) + if err != nil { + return nil, err + } + + return &instance.ListInstancesResponse{ + Instances: InstancesToPb(instances.Instances), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, instances.SearchResponse), + }, nil +} + +func (s *Server) ListCustomDomains(ctx context.Context, req *instance.ListCustomDomainsRequest) (*instance.ListCustomDomainsResponse, error) { + queries, err := ListCustomDomainsRequestToModel(req, s.systemDefaults) + if err != nil { + return nil, err + } + + domains, err := s.query.SearchInstanceDomains(ctx, queries) + if err != nil { + return nil, err + } + + return &instance.ListCustomDomainsResponse{ + Domains: DomainsToPb(domains.Domains), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, domains.SearchResponse), + }, nil +} + +func (s *Server) ListTrustedDomains(ctx context.Context, req *instance.ListTrustedDomainsRequest) (*instance.ListTrustedDomainsResponse, error) { + queries, err := ListTrustedDomainsRequestToModel(req, s.systemDefaults) + if err != nil { + return nil, err + } + + domains, err := s.query.SearchInstanceTrustedDomains(ctx, queries) + if err != nil { + return nil, err + } + + return &instance.ListTrustedDomainsResponse{ + TrustedDomain: trustedDomainsToPb(domains.Domains), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, domains.SearchResponse), + }, nil +} diff --git a/internal/api/grpc/instance/v2beta/server.go b/internal/api/grpc/instance/v2beta/server.go new file mode 100644 index 0000000000..aaeaa4cc8f --- /dev/null +++ b/internal/api/grpc/instance/v2beta/server.go @@ -0,0 +1,60 @@ +package instance + +import ( + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +var _ instance.InstanceServiceServer = (*Server)(nil) + +type Server struct { + instance.UnimplementedInstanceServiceServer + command *command.Commands + query *query.Queries + systemDefaults systemdefaults.SystemDefaults + defaultInstance command.InstanceSetup + externalDomain string +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + query *query.Queries, + database string, + defaultInstance command.InstanceSetup, + externalDomain string, +) *Server { + return &Server{ + command: command, + query: query, + defaultInstance: defaultInstance, + externalDomain: externalDomain, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + instance.RegisterInstanceServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return instance.InstanceService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return instance.InstanceService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return instance.InstanceService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return instance.RegisterInstanceServiceHandler +} diff --git a/internal/api/grpc/management/idp_converter.go b/internal/api/grpc/management/idp_converter.go index 4f68c5f919..37169dfc6d 100644 --- a/internal/api/grpc/management/idp_converter.go +++ b/internal/api/grpc/management/idp_converter.go @@ -481,6 +481,7 @@ func addSAMLProviderToCommand(req *mgmt_pb.AddSAMLProviderRequest) *command.SAML WithSignedRequest: req.WithSignedRequest, NameIDFormat: nameIDFormat, TransientMappingAttributeName: req.GetTransientMappingAttributeName(), + FederatedLogoutEnabled: req.GetFederatedLogoutEnabled(), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } @@ -498,6 +499,7 @@ func updateSAMLProviderToCommand(req *mgmt_pb.UpdateSAMLProviderRequest) *comman WithSignedRequest: req.WithSignedRequest, NameIDFormat: nameIDFormat, TransientMappingAttributeName: req.GetTransientMappingAttributeName(), + FederatedLogoutEnabled: req.GetFederatedLogoutEnabled(), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } diff --git a/internal/api/grpc/management/org_converter.go b/internal/api/grpc/management/org_converter.go index 879b5e0763..03de84cdf4 100644 --- a/internal/api/grpc/management/org_converter.go +++ b/internal/api/grpc/management/org_converter.go @@ -26,7 +26,7 @@ func ListOrgDomainsRequestToModel(req *mgmt_pb.ListOrgDomainsRequest) (*query.Or Limit: limit, Asc: asc, }, - //SortingColumn: //TODO: sorting + // SortingColumn: //TODO: sorting Queries: queries, }, nil } @@ -89,7 +89,7 @@ func ListOrgMembersRequestToModel(ctx context.Context, req *mgmt_pb.ListOrgMembe Offset: offset, Limit: limit, Asc: asc, - //SortingColumn: //TODO: sorting + // SortingColumn: //TODO: sorting }, Queries: queries, }, diff --git a/internal/api/grpc/management/project.go b/internal/api/grpc/management/project.go index 52b6b10e9a..f3af8dbf86 100644 --- a/internal/api/grpc/management/project.go +++ b/internal/api/grpc/management/project.go @@ -47,7 +47,7 @@ func (s *Server) ListProjects(ctx context.Context, req *mgmt_pb.ListProjectsRequ if err != nil { return nil, err } - projects, err := s.query.SearchProjects(ctx, queries) + projects, err := s.query.SearchProjects(ctx, queries, nil) if err != nil { return nil, err } @@ -109,7 +109,7 @@ func (s *Server) ListGrantedProjects(ctx context.Context, req *mgmt_pb.ListGrant if err != nil { return nil, err } - projects, err := s.query.SearchProjectGrants(ctx, queries) + projects, err := s.query.SearchProjectGrants(ctx, queries, nil) if err != nil { return nil, err } @@ -175,25 +175,26 @@ func (s *Server) ListProjectChanges(ctx context.Context, req *mgmt_pb.ListProjec } func (s *Server) AddProject(ctx context.Context, req *mgmt_pb.AddProjectRequest) (*mgmt_pb.AddProjectResponse, error) { - project, err := s.command.AddProject(ctx, ProjectCreateToDomain(req), authz.GetCtxData(ctx).OrgID) + add := ProjectCreateToCommand(req, "", authz.GetCtxData(ctx).OrgID) + project, err := s.command.AddProject(ctx, add) if err != nil { return nil, err } return &mgmt_pb.AddProjectResponse{ - Id: project.AggregateID, - Details: object_grpc.AddToDetailsPb(project.Sequence, project.ChangeDate, project.ResourceOwner), + Id: add.AggregateID, + Details: object_grpc.AddToDetailsPb(project.Sequence, project.EventDate, project.ResourceOwner), }, nil } func (s *Server) UpdateProject(ctx context.Context, req *mgmt_pb.UpdateProjectRequest) (*mgmt_pb.UpdateProjectResponse, error) { - project, err := s.command.ChangeProject(ctx, ProjectUpdateToDomain(req), authz.GetCtxData(ctx).OrgID) + project, err := s.command.ChangeProject(ctx, ProjectUpdateToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectResponse{ Details: object_grpc.ChangeToDetailsPb( project.Sequence, - project.ChangeDate, + project.EventDate, project.ResourceOwner, ), }, nil @@ -252,7 +253,7 @@ func (s *Server) ListProjectRoles(ctx context.Context, req *mgmt_pb.ListProjectR if err != nil { return nil, err } - roles, err := s.query.SearchProjectRoles(ctx, true, queries) + roles, err := s.query.SearchProjectRoles(ctx, true, queries, nil) if err != nil { return nil, err } @@ -263,21 +264,21 @@ func (s *Server) ListProjectRoles(ctx context.Context, req *mgmt_pb.ListProjectR } func (s *Server) AddProjectRole(ctx context.Context, req *mgmt_pb.AddProjectRoleRequest) (*mgmt_pb.AddProjectRoleResponse, error) { - role, err := s.command.AddProjectRole(ctx, AddProjectRoleRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + role, err := s.command.AddProjectRole(ctx, AddProjectRoleRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.AddProjectRoleResponse{ Details: object_grpc.AddToDetailsPb( role.Sequence, - role.ChangeDate, + role.EventDate, role.ResourceOwner, ), }, nil } func (s *Server) BulkAddProjectRoles(ctx context.Context, req *mgmt_pb.BulkAddProjectRolesRequest) (*mgmt_pb.BulkAddProjectRolesResponse, error) { - details, err := s.command.BulkAddProjectRole(ctx, req.ProjectId, authz.GetCtxData(ctx).OrgID, BulkAddProjectRolesRequestToDomain(req)) + details, err := s.command.BulkAddProjectRole(ctx, req.ProjectId, authz.GetCtxData(ctx).OrgID, BulkAddProjectRolesRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } @@ -287,14 +288,14 @@ func (s *Server) BulkAddProjectRoles(ctx context.Context, req *mgmt_pb.BulkAddPr } func (s *Server) UpdateProjectRole(ctx context.Context, req *mgmt_pb.UpdateProjectRoleRequest) (*mgmt_pb.UpdateProjectRoleResponse, error) { - role, err := s.command.ChangeProjectRole(ctx, UpdateProjectRoleRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + role, err := s.command.ChangeProjectRole(ctx, UpdateProjectRoleRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectRoleResponse{ Details: object_grpc.ChangeToDetailsPb( role.Sequence, - role.ChangeDate, + role.EventDate, role.ResourceOwner, ), }, nil diff --git a/internal/api/grpc/management/project_application.go b/internal/api/grpc/management/project_application.go index 3a0e1d5f92..ab49905409 100644 --- a/internal/api/grpc/management/project_application.go +++ b/internal/api/grpc/management/project_application.go @@ -271,7 +271,7 @@ func (s *Server) ListAppKeys(ctx context.Context, req *mgmt_pb.ListAppKeysReques if err != nil { return nil, err } - keys, err := s.query.SearchAuthNKeys(ctx, queries, false) + keys, err := s.query.SearchAuthNKeys(ctx, queries, query.JoinFilterApp, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/project_converter.go b/internal/api/grpc/management/project_converter.go index 64243ba258..83a8246feb 100644 --- a/internal/api/grpc/management/project_converter.go +++ b/internal/api/grpc/management/project_converter.go @@ -3,10 +3,13 @@ package management import ( "context" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/api/authz" member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member" "github.com/zitadel/zitadel/internal/api/grpc/object" proj_grpc "github.com/zitadel/zitadel/internal/api/grpc/project" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" @@ -14,8 +17,12 @@ import ( proj_pb "github.com/zitadel/zitadel/pkg/grpc/project" ) -func ProjectCreateToDomain(req *mgmt_pb.AddProjectRequest) *domain.Project { - return &domain.Project{ +func ProjectCreateToCommand(req *mgmt_pb.AddProjectRequest, projectID string, resourceOwner string) *command.AddProject { + return &command.AddProject{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + ResourceOwner: resourceOwner, + }, Name: req.Name, ProjectRoleAssertion: req.ProjectRoleAssertion, ProjectRoleCheck: req.ProjectRoleCheck, @@ -24,16 +31,17 @@ func ProjectCreateToDomain(req *mgmt_pb.AddProjectRequest) *domain.Project { } } -func ProjectUpdateToDomain(req *mgmt_pb.UpdateProjectRequest) *domain.Project { - return &domain.Project{ +func ProjectUpdateToCommand(req *mgmt_pb.UpdateProjectRequest, resourceOwner string) *command.ChangeProject { + return &command.ChangeProject{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.Id, + AggregateID: req.Id, + ResourceOwner: resourceOwner, }, - Name: req.Name, - ProjectRoleAssertion: req.ProjectRoleAssertion, - ProjectRoleCheck: req.ProjectRoleCheck, - HasProjectCheck: req.HasProjectCheck, - PrivateLabelingSetting: privateLabelingSettingToDomain(req.PrivateLabelingSetting), + Name: gu.Ptr(req.Name), + ProjectRoleAssertion: gu.Ptr(req.ProjectRoleAssertion), + ProjectRoleCheck: gu.Ptr(req.ProjectRoleCheck), + HasProjectCheck: gu.Ptr(req.HasProjectCheck), + PrivateLabelingSetting: gu.Ptr(privateLabelingSettingToDomain(req.PrivateLabelingSetting)), } } @@ -48,10 +56,11 @@ func privateLabelingSettingToDomain(setting proj_pb.PrivateLabelingSetting) doma } } -func AddProjectRoleRequestToDomain(req *mgmt_pb.AddProjectRoleRequest) *domain.ProjectRole { - return &domain.ProjectRole{ +func AddProjectRoleRequestToCommand(req *mgmt_pb.AddProjectRoleRequest, resourceOwner string) *command.AddProjectRole { + return &command.AddProjectRole{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, Key: req.RoleKey, DisplayName: req.DisplayName, @@ -59,12 +68,13 @@ func AddProjectRoleRequestToDomain(req *mgmt_pb.AddProjectRoleRequest) *domain.P } } -func BulkAddProjectRolesRequestToDomain(req *mgmt_pb.BulkAddProjectRolesRequest) []*domain.ProjectRole { - roles := make([]*domain.ProjectRole, len(req.Roles)) +func BulkAddProjectRolesRequestToCommand(req *mgmt_pb.BulkAddProjectRolesRequest, resourceOwner string) []*command.AddProjectRole { + roles := make([]*command.AddProjectRole, len(req.Roles)) for i, role := range req.Roles { - roles[i] = &domain.ProjectRole{ + roles[i] = &command.AddProjectRole{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, Key: role.Key, DisplayName: role.DisplayName, @@ -74,10 +84,11 @@ func BulkAddProjectRolesRequestToDomain(req *mgmt_pb.BulkAddProjectRolesRequest) return roles } -func UpdateProjectRoleRequestToDomain(req *mgmt_pb.UpdateProjectRoleRequest) *domain.ProjectRole { - return &domain.ProjectRole{ +func UpdateProjectRoleRequestToCommand(req *mgmt_pb.UpdateProjectRoleRequest, resourceOwner string) *command.ChangeProjectRole { + return &command.ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, Key: req.RoleKey, DisplayName: req.DisplayName, diff --git a/internal/api/grpc/management/project_grant.go b/internal/api/grpc/management/project_grant.go index cea8e929a4..d84375818d 100644 --- a/internal/api/grpc/management/project_grant.go +++ b/internal/api/grpc/management/project_grant.go @@ -31,7 +31,7 @@ func (s *Server) ListProjectGrants(ctx context.Context, req *mgmt_pb.ListProject if err != nil { return nil, err } - grants, err := s.query.SearchProjectGrants(ctx, queries) + grants, err := s.query.SearchProjectGrants(ctx, queries, nil) if err != nil { return nil, err } @@ -54,7 +54,7 @@ func (s *Server) ListAllProjectGrants(ctx context.Context, req *mgmt_pb.ListAllP if err != nil { return nil, err } - grants, err := s.query.SearchProjectGrants(ctx, queries) + grants, err := s.query.SearchProjectGrants(ctx, queries, nil) if err != nil { return nil, err } @@ -65,16 +65,17 @@ func (s *Server) ListAllProjectGrants(ctx context.Context, req *mgmt_pb.ListAllP } func (s *Server) AddProjectGrant(ctx context.Context, req *mgmt_pb.AddProjectGrantRequest) (*mgmt_pb.AddProjectGrantResponse, error) { - grant, err := s.command.AddProjectGrant(ctx, AddProjectGrantRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + grant := AddProjectGrantRequestToCommand(req, "", authz.GetCtxData(ctx).OrgID) + details, err := s.command.AddProjectGrant(ctx, grant) if err != nil { return nil, err } return &mgmt_pb.AddProjectGrantResponse{ GrantId: grant.GrantID, Details: object_grpc.AddToDetailsPb( - grant.Sequence, - grant.ChangeDate, - grant.ResourceOwner, + details.Sequence, + details.EventDate, + details.ResourceOwner, ), }, nil } @@ -94,21 +95,21 @@ func (s *Server) UpdateProjectGrant(ctx context.Context, req *mgmt_pb.UpdateProj if err != nil { return nil, err } - grant, err := s.command.ChangeProjectGrant(ctx, UpdateProjectGrantRequestToDomain(req), authz.GetCtxData(ctx).OrgID, userGrantsToIDs(grants.UserGrants)...) + grant, err := s.command.ChangeProjectGrant(ctx, UpdateProjectGrantRequestToCommand(req, authz.GetCtxData(ctx).OrgID), userGrantsToIDs(grants.UserGrants)...) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectGrantResponse{ Details: object_grpc.ChangeToDetailsPb( grant.Sequence, - grant.ChangeDate, + grant.EventDate, grant.ResourceOwner, ), }, nil } func (s *Server) DeactivateProjectGrant(ctx context.Context, req *mgmt_pb.DeactivateProjectGrantRequest) (*mgmt_pb.DeactivateProjectGrantResponse, error) { - details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantId, authz.GetCtxData(ctx).OrgID) + details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantId, "", authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -118,7 +119,7 @@ func (s *Server) DeactivateProjectGrant(ctx context.Context, req *mgmt_pb.Deacti } func (s *Server) ReactivateProjectGrant(ctx context.Context, req *mgmt_pb.ReactivateProjectGrantRequest) (*mgmt_pb.ReactivateProjectGrantResponse, error) { - details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantId, authz.GetCtxData(ctx).OrgID) + details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantId, "", authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/project_grant_converter.go b/internal/api/grpc/management/project_grant_converter.go index de7d1de041..04bc35301f 100644 --- a/internal/api/grpc/management/project_grant_converter.go +++ b/internal/api/grpc/management/project_grant_converter.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member" "github.com/zitadel/zitadel/internal/api/grpc/object" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" @@ -100,20 +101,23 @@ func AllProjectGrantQueryToModel(apiQuery *proj_pb.AllProjectGrantQuery) (query. return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-M099f", "List.Query.Invalid") } } -func AddProjectGrantRequestToDomain(req *mgmt_pb.AddProjectGrantRequest) *domain.ProjectGrant { - return &domain.ProjectGrant{ +func AddProjectGrantRequestToCommand(req *mgmt_pb.AddProjectGrantRequest, grantID string, resourceOwner string) *command.AddProjectGrant { + return &command.AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, + GrantID: grantID, GrantedOrgID: req.GrantedOrgId, RoleKeys: req.RoleKeys, } } -func UpdateProjectGrantRequestToDomain(req *mgmt_pb.UpdateProjectGrantRequest) *domain.ProjectGrant { - return &domain.ProjectGrant{ +func UpdateProjectGrantRequestToCommand(req *mgmt_pb.UpdateProjectGrantRequest, resourceOwner string) *command.ChangeProjectGrant { + return &command.ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, GrantID: req.GrantId, RoleKeys: req.RoleKeys, diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 5b82eb5afe..09b9faa756 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -273,7 +273,8 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs if err != nil { return nil, err } - addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, links, initCodeGenerator, phoneCodeGenerator, emailCodeGenerator, passwordlessInitCode) + //nolint:staticcheck + addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, nil, links, initCodeGenerator, phoneCodeGenerator, emailCodeGenerator, passwordlessInitCode) if err != nil { return nil, err } @@ -297,7 +298,7 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs func (s *Server) AddMachineUser(ctx context.Context, req *mgmt_pb.AddMachineUserRequest) (*mgmt_pb.AddMachineUserResponse, error) { machine := AddMachineUserRequestToCommand(req, authz.GetCtxData(ctx).OrgID) - objectDetails, err := s.command.AddMachine(ctx, machine) + objectDetails, err := s.command.AddMachine(ctx, machine, nil, nil) if err != nil { return nil, err } @@ -752,11 +753,11 @@ func (s *Server) GetMachineKeyByIDs(ctx context.Context, req *mgmt_pb.GetMachine } func (s *Server) ListMachineKeys(ctx context.Context, req *mgmt_pb.ListMachineKeysRequest) (*mgmt_pb.ListMachineKeysResponse, error) { - query, err := ListMachineKeysRequestToQuery(ctx, req) + q, err := ListMachineKeysRequestToQuery(ctx, req) if err != nil { return nil, err } - result, err := s.query.SearchAuthNKeys(ctx, query, false) + result, err := s.query.SearchAuthNKeys(ctx, q, query.JoinFilterUserMachine, nil) if err != nil { return nil, err } @@ -774,7 +775,6 @@ func (s *Server) AddMachineKey(ctx context.Context, req *mgmt_pb.AddMachineKeyRe if err != nil { return nil, err } - // Return key details only if the pubkey wasn't supplied, otherwise the user already has // private key locally var keyDetails []byte @@ -821,7 +821,7 @@ func (s *Server) GenerateMachineSecret(ctx context.Context, req *mgmt_pb.Generat } func (s *Server) RemoveMachineSecret(ctx context.Context, req *mgmt_pb.RemoveMachineSecretRequest) (*mgmt_pb.RemoveMachineSecretResponse, error) { - objectDetails, err := s.command.RemoveMachineSecret(ctx, req.UserId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.RemoveMachineSecret(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, nil) if err != nil { return nil, err } @@ -839,7 +839,7 @@ func (s *Server) GetPersonalAccessTokenByIDs(ctx context.Context, req *mgmt_pb.G if err != nil { return nil, err } - token, err := s.query.PersonalAccessTokenByID(ctx, true, req.TokenId, false, resourceOwner, aggregateID) + token, err := s.query.PersonalAccessTokenByID(ctx, true, req.TokenId, resourceOwner, aggregateID) if err != nil { return nil, err } @@ -853,7 +853,7 @@ func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *mgmt_pb.List if err != nil { return nil, err } - result, err := s.query.SearchPersonalAccessTokens(ctx, queries, false) + result, err := s.query.SearchPersonalAccessTokens(ctx, queries, nil) if err != nil { return nil, err } @@ -901,6 +901,7 @@ func (s *Server) ListHumanLinkedIDPs(ctx context.Context, req *mgmt_pb.ListHuman Details: obj_grpc.ToListDetails(res.Count, res.Sequence, res.LastRun), }, nil } + func (s *Server) RemoveHumanLinkedIDP(ctx context.Context, req *mgmt_pb.RemoveHumanLinkedIDPRequest) (*mgmt_pb.RemoveHumanLinkedIDPResponse, error) { objectDetails, err := s.command.RemoveUserIDPLink(ctx, RemoveHumanLinkedIDPRequestToDomain(ctx, req)) if err != nil { @@ -947,18 +948,21 @@ func cascadingIAMMembership(membership *query.IAMMembership) *command.CascadingI } return &command.CascadingIAMMembership{IAMID: membership.IAMID} } + func cascadingOrgMembership(membership *query.OrgMembership) *command.CascadingOrgMembership { if membership == nil { return nil } return &command.CascadingOrgMembership{OrgID: membership.OrgID} } + func cascadingProjectMembership(membership *query.ProjectMembership) *command.CascadingProjectMembership { if membership == nil { return nil } return &command.CascadingProjectMembership{ProjectID: membership.ProjectID} } + func cascadingProjectGrantMembership(membership *query.ProjectGrantMembership) *command.CascadingProjectGrantMembership { if membership == nil { return nil diff --git a/internal/api/grpc/metadata/v2beta/metadata.go b/internal/api/grpc/metadata/v2beta/metadata.go new file mode 100644 index 0000000000..57da21dfd2 --- /dev/null +++ b/internal/api/grpc/metadata/v2beta/metadata.go @@ -0,0 +1,49 @@ +package metadata + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + v2beta_object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + meta_pb "github.com/zitadel/zitadel/pkg/grpc/metadata/v2beta" +) + +// code in this file is copied from internal/api/grpc/metadata/metadata.go + +func OrgMetadataListToPb(dataList []*query.OrgMetadata) []*meta_pb.Metadata { + mds := make([]*meta_pb.Metadata, len(dataList)) + for i, data := range dataList { + mds[i] = OrgMetadataToPb(data) + } + return mds +} + +func OrgMetadataToPb(data *query.OrgMetadata) *meta_pb.Metadata { + return &meta_pb.Metadata{ + Key: data.Key, + Value: data.Value, + CreationDate: timestamppb.New(data.CreationDate), + ChangeDate: timestamppb.New(data.ChangeDate), + } +} + +func OrgMetadataQueriesToQuery(queries []*meta_pb.MetadataQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = OrgMetadataQueryToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func OrgMetadataQueryToQuery(metadataQuery *meta_pb.MetadataQuery) (query.SearchQuery, error) { + switch q := metadataQuery.Query.(type) { + case *meta_pb.MetadataQuery_KeyQuery: + return query.NewOrgMetadataKeySearchQuery(q.KeyQuery.Key, v2beta_object.TextMethodToQuery(q.KeyQuery.Method)) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "METAD-fdg23", "List.Query.Invalid") + } +} diff --git a/internal/api/grpc/object/v2beta/converter.go b/internal/api/grpc/object/v2beta/converter.go index 9b14bb677a..73d5f18843 100644 --- a/internal/api/grpc/object/v2beta/converter.go +++ b/internal/api/grpc/object/v2beta/converter.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + org_pb "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" ) func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details { @@ -34,6 +35,7 @@ func ToListDetails(response query.SearchResponse) *object.ListDetails { return details } + func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool) { if query == nil { return 0, 0, false @@ -73,3 +75,56 @@ func TextMethodToQuery(method object.TextQueryMethod) query.TextComparison { return -1 } } + +func ListQueryToModel(query *object.ListQuery) (offset, limit uint64, asc bool) { + if query == nil { + return 0, 0, false + } + return query.Offset, uint64(query.Limit), query.Asc +} + +func DomainsToPb(domains []*query.Domain) []*org_pb.Domain { + d := make([]*org_pb.Domain, len(domains)) + for i, domain := range domains { + d[i] = DomainToPb(domain) + } + return d +} + +func DomainToPb(d *query.Domain) *org_pb.Domain { + return &org_pb.Domain{ + OrganizationId: d.OrgID, + DomainName: d.Domain, + IsVerified: d.IsVerified, + IsPrimary: d.IsPrimary, + ValidationType: DomainValidationTypeFromModel(d.ValidationType), + } +} + +func DomainValidationTypeFromModel(validationType domain.OrgDomainValidationType) org_pb.DomainValidationType { + switch validationType { + case domain.OrgDomainValidationTypeDNS: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS + case domain.OrgDomainValidationTypeHTTP: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP + case domain.OrgDomainValidationTypeUnspecified: + // added to please golangci-lint + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED + default: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED + } +} + +func DomainValidationTypeToDomain(validationType org_pb.DomainValidationType) domain.OrgDomainValidationType { + switch validationType { + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP: + return domain.OrgDomainValidationTypeHTTP + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS: + return domain.OrgDomainValidationTypeDNS + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED: + // added to please golangci-lint + return domain.OrgDomainValidationTypeUnspecified + default: + return domain.OrgDomainValidationTypeUnspecified + } +} 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 64334bd8b1..187dc922fc 100644 --- a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go @@ -24,8 +24,7 @@ import ( ) func TestServer_GetAuthRequest(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) @@ -98,8 +97,7 @@ func TestServer_GetAuthRequest(t *testing.T) { } func TestServer_CreateCallback(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) clientV2, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) @@ -288,7 +286,7 @@ func TestServer_CreateCallback(t *testing.T) { ctx: CTX, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil) + client, err := Instance.CreateOIDCImplicitFlowClient(CTX, t, redirectURIImplicit, nil) require.NoError(t, err) authRequestID, err := Instance.CreateOIDCAuthRequestImplicit(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURIImplicit) require.NoError(t, err) @@ -315,7 +313,7 @@ func TestServer_CreateCallback(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, loginV2) + clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, t, redirectURIImplicit, loginV2) require.NoError(t, err) authRequestID, err := Instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURIImplicit) require.NoError(t, err) @@ -371,7 +369,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID2, _ := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID2, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID2, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -386,7 +384,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -407,9 +405,9 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) }, @@ -544,9 +542,9 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, false) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) }, want: &oidc_pb.CreateCallbackResponse{ @@ -564,7 +562,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, false) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) }, @@ -606,7 +604,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, false, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) @@ -639,8 +637,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { } func TestServer_GetDeviceAuthorizationRequest(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) 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) @@ -697,8 +694,7 @@ func TestServer_GetDeviceAuthorizationRequest(t *testing.T) { } func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) 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) @@ -895,8 +891,7 @@ func createSessionAndAuthRequestForCallback(ctx context.Context, t *testing.T, c } func createOIDCApplication(ctx context.Context, t *testing.T, projectRoleCheck, hasProjectCheck bool) (string, string) { - project, err := Instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) - require.NoError(t, err) + project := Instance.CreateProject(ctx, t, "", gofakeit.AppName(), projectRoleCheck, hasProjectCheck) clientV2, err := Instance.CreateOIDCClientLoginVersion(ctx, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) require.NoError(t, err) return project.GetId(), clientV2.GetClientId() 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 d7d746e2d0..bd02f9e068 100644 --- a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go @@ -23,8 +23,7 @@ import ( ) func TestServer_GetAuthRequest(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) @@ -97,8 +96,7 @@ func TestServer_GetAuthRequest(t *testing.T) { } func TestServer_CreateCallback(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) clientV2, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) @@ -289,7 +287,7 @@ func TestServer_CreateCallback(t *testing.T) { ctx: CTX, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil) + client, err := Instance.CreateOIDCImplicitFlowClient(CTX, t, redirectURIImplicit, nil) require.NoError(t, err) authRequestID, err := Instance.CreateOIDCAuthRequestImplicit(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURIImplicit) require.NoError(t, err) @@ -316,7 +314,7 @@ func TestServer_CreateCallback(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, loginV2) + clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, t, redirectURIImplicit, loginV2) require.NoError(t, err) authRequestID, err := Instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURIImplicit) require.NoError(t, err) @@ -372,7 +370,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID2, _ := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID2, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID2, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -387,7 +385,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -408,9 +406,9 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) }, @@ -545,9 +543,9 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, false) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) }, want: &oidc_pb.CreateCallbackResponse{ @@ -565,7 +563,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, false) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) }, @@ -607,7 +605,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, false, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) @@ -669,8 +667,7 @@ func createSessionAndAuthRequestForCallback(ctx context.Context, t *testing.T, c } func createOIDCApplication(ctx context.Context, t *testing.T, projectRoleCheck, hasProjectCheck bool) (string, string) { - project, err := Instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) - require.NoError(t, err) + project := Instance.CreateProject(ctx, t, "", gofakeit.AppName(), projectRoleCheck, hasProjectCheck) clientV2, err := Instance.CreateOIDCClientLoginVersion(ctx, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) require.NoError(t, err) return project.GetId(), clientV2.GetClientId() diff --git a/internal/api/grpc/org/v2/integration_test/org_test.go b/internal/api/grpc/org/v2/integration_test/org_test.go index aa8a718e68..b28bbf5ef2 100644 --- a/internal/api/grpc/org/v2/integration_test/org_test.go +++ b/internal/api/grpc/org/v2/integration_test/org_test.go @@ -81,6 +81,18 @@ func TestServer_AddOrganization(t *testing.T) { }, wantErr: true, }, + { + name: "no admin, custom org ID", + ctx: CTX, + req: &org.AddOrganizationRequest{ + Name: gofakeit.AppName(), + OrgId: gu.Ptr("custom-org-ID"), + }, + want: &org.AddOrganizationResponse{ + OrganizationId: "custom-org-ID", + CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{}, + }, + }, { name: "admin with init with userID passed for Human admin", ctx: CTX, 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 86a2bd312b..b2f27dbe62 100644 --- a/internal/api/grpc/org/v2/integration_test/query_test.go +++ b/internal/api/grpc/org/v2/integration_test/query_test.go @@ -5,7 +5,9 @@ package org_test import ( "context" "fmt" + "slices" "strconv" + "strings" "testing" "time" @@ -36,6 +38,16 @@ func createOrganization(ctx context.Context, name string) orgAttr { } } +func createOrganizationWithCustomOrgID(ctx context.Context, name string, orgID string) orgAttr { + orgResp := Instance.CreateOrganizationWithCustomOrgID(ctx, name, orgID) + 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 @@ -89,6 +101,7 @@ func TestServer_ListOrganizations(t *testing.T) { Queries: []*org.SearchQuery{ OrganizationIdQuery(Instance.DefaultOrg.Id), }, + SortingColumn: org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_NAME, }, func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { count := 3 @@ -101,6 +114,10 @@ func TestServer_ListOrganizations(t *testing.T) { request.Queries = []*org.SearchQuery{ OrganizationNamePrefixQuery(prefix), } + + slices.SortFunc(orgs, func(a, b orgAttr) int { + return -1 * strings.Compare(a.Name, b.Name) + }) return orgs, nil }, }, @@ -156,6 +173,35 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, + { + name: "list org by custom id, ok", + args: args{ + CTX, + &org.ListOrganizationsRequest{}, + func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { + orgs := make([]orgAttr, 1) + name := fmt.Sprintf("ListOrgs-%s", gofakeit.AppName()) + orgID := gofakeit.Company() + orgs[0] = createOrganizationWithCustomOrgID(ctx, name, orgID) + request.Queries = []*org.SearchQuery{ + OrganizationIdQuery(orgID), + } + return orgs, nil + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*org.Organization{ + { + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + }, + }, + }, + }, { name: "list org by name, ok", args: args{ diff --git a/internal/api/grpc/org/v2/org.go b/internal/api/grpc/org/v2/org.go index 5f21f7403e..b876826365 100644 --- a/internal/api/grpc/org/v2/org.go +++ b/internal/api/grpc/org/v2/org.go @@ -31,6 +31,7 @@ func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*comm Name: request.GetName(), CustomDomain: "", Admins: admins, + OrgID: request.GetOrgId(), }, nil } @@ -68,12 +69,15 @@ func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admi } func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.AddOrganizationResponse, err error) { - admins := make([]*org.AddOrganizationResponse_CreatedAdmin, len(createdOrg.CreatedAdmins)) - for i, admin := range createdOrg.CreatedAdmins { - admins[i] = &org.AddOrganizationResponse_CreatedAdmin{ - UserId: admin.ID, - EmailCode: admin.EmailCode, - PhoneCode: admin.PhoneCode, + admins := make([]*org.AddOrganizationResponse_CreatedAdmin, 0, len(createdOrg.OrgAdmins)) + for _, admin := range createdOrg.OrgAdmins { + admin, ok := admin.(*command.CreatedOrgAdmin) + if ok { + admins = append(admins, &org.AddOrganizationResponse_CreatedAdmin{ + UserId: admin.GetID(), + EmailCode: admin.EmailCode, + PhoneCode: admin.PhoneCode, + }) } } return &org.AddOrganizationResponse{ diff --git a/internal/api/grpc/org/v2/org_test.go b/internal/api/grpc/org/v2/org_test.go index b384f858de..37a3dca41a 100644 --- a/internal/api/grpc/org/v2/org_test.go +++ b/internal/api/grpc/org/v2/org_test.go @@ -38,6 +38,21 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { }, wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil), }, + { + name: "custom org ID", + args: args{ + request: &org.AddOrganizationRequest{ + Name: "custom org ID", + OrgId: gu.Ptr("org-ID"), + }, + }, + want: &command.OrgSetup{ + Name: "custom org ID", + CustomDomain: "", + Admins: []*command.OrgSetupAdmin{}, + OrgID: "org-ID", + }, + }, { name: "user ID", args: args{ @@ -135,8 +150,8 @@ func Test_createdOrganizationToPb(t *testing.T) { EventDate: now, ResourceOwner: "orgID", }, - CreatedAdmins: []*command.CreatedOrgAdmin{ - { + OrgAdmins: []command.OrgAdmin{ + &command.CreatedOrgAdmin{ ID: "id", EmailCode: gu.Ptr("emailCode"), PhoneCode: gu.Ptr("phoneCode"), diff --git a/internal/api/grpc/org/v2beta/helper.go b/internal/api/grpc/org/v2beta/helper.go new file mode 100644 index 0000000000..6f47819bb4 --- /dev/null +++ b/internal/api/grpc/org/v2beta/helper.go @@ -0,0 +1,271 @@ +package org + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + // TODO fix below + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + metadata "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2beta" + v2beta_object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + v2beta "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" +) + +// NOTE: most of this code is copied from `internal/api/grpc/admin/*`, as we will eventually axe the previous versons of the API, +// we will have code duplication until then + +func listOrgRequestToModel(systemDefaults systemdefaults.SystemDefaults, request *v2beta_org.ListOrganizationsRequest) (*query.OrgSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := OrgQueriesToModel(request.Filter) + if err != nil { + return nil, err + } + return &query.OrgSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + SortingColumn: FieldNameToOrgColumn(request.SortingColumn), + Asc: asc, + }, + Queries: queries, + }, nil +} + +func OrganizationViewToPb(org *query.Org) *v2beta_org.Organization { + return &v2beta_org.Organization{ + Id: org.ID, + State: OrgStateToPb(org.State), + Name: org.Name, + PrimaryDomain: org.Domain, + CreationDate: timestamppb.New(org.CreationDate), + ChangedDate: timestamppb.New(org.ChangeDate), + } +} + +func OrgStateToPb(state domain.OrgState) v2beta_org.OrgState { + switch state { + case domain.OrgStateActive: + return v2beta_org.OrgState_ORG_STATE_ACTIVE + case domain.OrgStateInactive: + return v2beta_org.OrgState_ORG_STATE_INACTIVE + case domain.OrgStateRemoved: + // added to please golangci-lint + return v2beta_org.OrgState_ORG_STATE_REMOVED + case domain.OrgStateUnspecified: + // added to please golangci-lint + return v2beta_org.OrgState_ORG_STATE_UNSPECIFIED + default: + return v2beta_org.OrgState_ORG_STATE_UNSPECIFIED + } +} + +func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.CreateOrganizationResponse, err error) { + admins := make([]*org.OrganizationAdmin, len(createdOrg.OrgAdmins)) + for i, admin := range createdOrg.OrgAdmins { + switch admin := admin.(type) { + case *command.CreatedOrgAdmin: + admins[i] = &org.OrganizationAdmin{ + OrganizationAdmin: &org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &org.CreatedAdmin{ + UserId: admin.ID, + EmailCode: admin.EmailCode, + PhoneCode: admin.PhoneCode, + }, + }, + } + case *command.AssignedOrgAdmin: + admins[i] = &org.OrganizationAdmin{ + OrganizationAdmin: &org.OrganizationAdmin_AssignedAdmin{ + AssignedAdmin: &org.AssignedAdmin{ + UserId: admin.ID, + }, + }, + } + } + } + return &org.CreateOrganizationResponse{ + CreationDate: timestamppb.New(createdOrg.ObjectDetails.EventDate), + Id: createdOrg.ObjectDetails.ResourceOwner, + OrganizationAdmins: admins, + }, nil +} + +func OrgViewsToPb(orgs []*query.Org) []*v2beta_org.Organization { + o := make([]*v2beta_org.Organization, len(orgs)) + for i, org := range orgs { + o[i] = OrganizationViewToPb(org) + } + return o +} + +func OrgQueriesToModel(queries []*v2beta_org.OrganizationSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = OrgQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func OrgQueryToModel(apiQuery *v2beta_org.OrganizationSearchFilter) (query.SearchQuery, error) { + switch q := apiQuery.Filter.(type) { + case *v2beta_org.OrganizationSearchFilter_DomainFilter: + return query.NewOrgVerifiedDomainSearchQuery(v2beta_object.TextMethodToQuery(q.DomainFilter.Method), q.DomainFilter.Domain) + case *v2beta_org.OrganizationSearchFilter_NameFilter: + return query.NewOrgNameSearchQuery(v2beta_object.TextMethodToQuery(q.NameFilter.Method), q.NameFilter.Name) + case *v2beta_org.OrganizationSearchFilter_StateFilter: + return query.NewOrgStateSearchQuery(OrgStateToDomain(q.StateFilter.State)) + case *v2beta_org.OrganizationSearchFilter_IdFilter: + return query.NewOrgIDSearchQuery(q.IdFilter.Id) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func OrgStateToDomain(state v2beta_org.OrgState) domain.OrgState { + switch state { + case v2beta_org.OrgState_ORG_STATE_ACTIVE: + return domain.OrgStateActive + case v2beta_org.OrgState_ORG_STATE_INACTIVE: + return domain.OrgStateInactive + case v2beta_org.OrgState_ORG_STATE_REMOVED: + // added to please golangci-lint + return domain.OrgStateRemoved + case v2beta_org.OrgState_ORG_STATE_UNSPECIFIED: + fallthrough + default: + return domain.OrgStateUnspecified + } +} + +func FieldNameToOrgColumn(fieldName v2beta_org.OrgFieldName) query.Column { + switch fieldName { + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_NAME: + return query.OrgColumnName + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_CREATION_DATE: + return query.OrgColumnCreationDate + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_UNSPECIFIED: + return query.Column{} + default: + return query.Column{} + } +} + +func ListOrgDomainsRequestToModel(systemDefaults systemdefaults.SystemDefaults, request *org.ListOrganizationDomainsRequest) (*query.OrgDomainSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := DomainQueriesToModel(request.Filters) + if err != nil { + return nil, err + } + return &query.OrgDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + // SortingColumn: //TODO: sorting + Queries: queries, + }, nil +} + +func ListQueryToModel(query *v2beta.ListQuery) (offset, limit uint64, asc bool) { + if query == nil { + return 0, 0, false + } + return query.Offset, uint64(query.Limit), query.Asc +} + +func DomainQueriesToModel(queries []*v2beta_org.DomainSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = DomainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func DomainQueryToModel(searchQuery *v2beta_org.DomainSearchFilter) (query.SearchQuery, error) { + switch q := searchQuery.Filter.(type) { + case *v2beta_org.DomainSearchFilter_DomainNameFilter: + return query.NewOrgDomainDomainSearchQuery(v2beta_object.TextMethodToQuery(q.DomainNameFilter.Method), q.DomainNameFilter.Name) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-Ags89", "List.Query.Invalid") + } +} + +func RemoveOrgDomainRequestToDomain(ctx context.Context, req *v2beta_org.DeleteOrganizationDomainRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + } +} + +func GenerateOrgDomainValidationRequestToDomain(ctx context.Context, req *v2beta_org.GenerateOrganizationDomainValidationRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + ValidationType: v2beta_object.DomainValidationTypeToDomain(req.Type), + } +} + +func ValidateOrgDomainRequestToDomain(ctx context.Context, req *v2beta_org.VerifyOrganizationDomainRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + } +} + +func BulkSetOrgMetadataToDomain(req *v2beta_org.SetOrganizationMetadataRequest) []*domain.Metadata { + metadata := make([]*domain.Metadata, len(req.Metadata)) + for i, data := range req.Metadata { + metadata[i] = &domain.Metadata{ + Key: data.Key, + Value: data.Value, + } + } + return metadata +} + +func ListOrgMetadataToDomain(systemDefaults systemdefaults.SystemDefaults, request *v2beta_org.ListOrganizationMetadataRequest) (*query.OrgMetadataSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := metadata.OrgMetadataQueriesToQuery(request.Filter) + if err != nil { + return nil, err + } + return &query.OrgMetadataSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: queries, + }, nil +} diff --git a/internal/api/grpc/org/v2beta/integration_test/org_test.go b/internal/api/grpc/org/v2beta/integration_test/org_test.go index 5998b17a71..0d3b920afe 100644 --- a/internal/api/grpc/org/v2beta/integration_test/org_test.go +++ b/internal/api/grpc/org/v2beta/integration_test/org_test.go @@ -4,7 +4,9 @@ package org_test import ( "context" + "errors" "os" + "strings" "testing" "time" @@ -14,7 +16,9 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/integration" - org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/admin" + v2beta_object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) @@ -22,7 +26,7 @@ import ( var ( CTX context.Context Instance *integration.Instance - Client org.OrganizationServiceClient + Client v2beta_org.OrganizationServiceClient User *user.AddHumanUserResponse ) @@ -40,20 +44,21 @@ func TestMain(m *testing.M) { }()) } -func TestServer_AddOrganization(t *testing.T) { +func TestServer_CreateOrganization(t *testing.T) { idpResp := Instance.AddGenericOAuthProvider(CTX, Instance.DefaultOrg.Id) tests := []struct { name string ctx context.Context - req *org.AddOrganizationRequest - want *org.AddOrganizationResponse + req *v2beta_org.CreateOrganizationRequest + id string + want *v2beta_org.CreateOrganizationResponse wantErr bool }{ { name: "missing permission", - ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &org.AddOrganizationRequest{ + ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &v2beta_org.CreateOrganizationRequest{ Name: "name", Admins: nil, }, @@ -62,7 +67,7 @@ func TestServer_AddOrganization(t *testing.T) { { name: "empty name", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: "", Admins: nil, }, @@ -71,22 +76,45 @@ func TestServer_AddOrganization(t *testing.T) { { name: "invalid admin type", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ {}, }, }, wantErr: true, }, + { + name: "existing user as admin", + ctx: CTX, + req: &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ + { + UserType: &v2beta_org.CreateOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, + }, + }, + }, + want: &v2beta_org.CreateOrganizationResponse{ + OrganizationAdmins: []*v2beta_org.OrganizationAdmin{ + { + OrganizationAdmin: &v2beta_org.OrganizationAdmin_AssignedAdmin{ + AssignedAdmin: &v2beta_org.AssignedAdmin{ + UserId: User.GetUserId(), + }, + }, + }, + }, + }, + }, { name: "admin with init", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &v2beta_org.CreateOrganizationRequest_Admin_Human{ Human: &user_v2beta.AddHumanUserRequest{ Profile: &user_v2beta.SetHumanProfile{ GivenName: "firstname", @@ -103,13 +131,17 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - OrganizationId: integration.NotEmpty, - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: &v2beta_org.CreateOrganizationResponse{ + Id: integration.NotEmpty, + OrganizationAdmins: []*v2beta_org.OrganizationAdmin{ { - UserId: integration.NotEmpty, - EmailCode: gu.Ptr(integration.NotEmpty), - PhoneCode: nil, + OrganizationAdmin: &v2beta_org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &v2beta_org.CreatedAdmin{ + UserId: integration.NotEmpty, + EmailCode: gu.Ptr(integration.NotEmpty), + PhoneCode: nil, + }, + }, }, }, }, @@ -117,14 +149,14 @@ func TestServer_AddOrganization(t *testing.T) { { name: "existing user and new human with idp", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, + UserType: &v2beta_org.CreateOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, }, { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &v2beta_org.CreateOrganizationRequest_Admin_Human{ Human: &user_v2beta.AddHumanUserRequest{ Profile: &user_v2beta.SetHumanProfile{ GivenName: "firstname", @@ -148,19 +180,124 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ - // a single admin is expected, because the first provided already exists + want: &v2beta_org.CreateOrganizationResponse{ + // OrganizationId: integration.NotEmpty, + OrganizationAdmins: []*v2beta_org.OrganizationAdmin{ { - UserId: integration.NotEmpty, + OrganizationAdmin: &v2beta_org.OrganizationAdmin_AssignedAdmin{ + AssignedAdmin: &v2beta_org.AssignedAdmin{ + UserId: User.GetUserId(), + }, + }, + }, + { + OrganizationAdmin: &v2beta_org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &v2beta_org.CreatedAdmin{ + UserId: integration.NotEmpty, + }, + }, }, }, }, }, + { + name: "create with ID", + ctx: CTX, + id: "custom_id", + req: &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + Id: gu.Ptr("custom_id"), + }, + want: &v2beta_org.CreateOrganizationResponse{}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Client.AddOrganization(tt.ctx, tt.req) + got, err := Client.CreateOrganization(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + if tt.id != "" { + require.Equal(t, tt.id, got.Id) + } + + // check details + gotCD := got.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check the admins + require.Equal(t, len(tt.want.GetOrganizationAdmins()), len(got.GetOrganizationAdmins())) + for i, admin := range tt.want.GetOrganizationAdmins() { + gotAdmin := got.GetOrganizationAdmins()[i].OrganizationAdmin + switch admin := admin.OrganizationAdmin.(type) { + case *v2beta_org.OrganizationAdmin_CreatedAdmin: + assertCreatedAdmin(t, admin.CreatedAdmin, gotAdmin.(*v2beta_org.OrganizationAdmin_CreatedAdmin).CreatedAdmin) + case *v2beta_org.OrganizationAdmin_AssignedAdmin: + assert.Equal(t, admin.AssignedAdmin.GetUserId(), gotAdmin.(*v2beta_org.OrganizationAdmin_AssignedAdmin).AssignedAdmin.GetUserId()) + } + } + }) + } +} + +func TestServer_UpdateOrganization(t *testing.T) { + orgs, orgsName, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + orgName := orgsName[0] + + tests := []struct { + name string + ctx context.Context + req *v2beta_org.UpdateOrganizationRequest + want *v2beta_org.UpdateOrganizationResponse + wantErr bool + }{ + { + name: "update org with new name", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: orgId, + Name: "new org name", + }, + }, + { + name: "update org with same name", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: orgId, + Name: orgName, + }, + }, + { + name: "update org with non existent org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: "non existant org id", + // Name: "", + }, + wantErr: true, + }, + { + name: "update org with no id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: "", + Name: orgName, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.UpdateOrganization(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return @@ -168,26 +305,1678 @@ func TestServer_AddOrganization(t *testing.T) { require.NoError(t, err) // check details - assert.NotZero(t, got.GetDetails().GetSequence()) - gotCD := got.GetDetails().GetChangeDate().AsTime() + gotCD := got.GetChangeDate().AsTime() now := time.Now() assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) - assert.NotEmpty(t, got.GetDetails().GetResourceOwner()) + }) + } +} - // organization id must be the same as the resourceOwner - assert.Equal(t, got.GetDetails().GetResourceOwner(), got.GetOrganizationId()) +func TestServer_ListOrganizations(t *testing.T) { + testStartTimestamp := time.Now() + ListOrgIinstance := integration.NewInstance(CTX) + listOrgIAmOwnerCtx := ListOrgIinstance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + listOrgClient := ListOrgIinstance.Client.OrgV2beta - // check the admins - require.Len(t, got.GetCreatedAdmins(), len(tt.want.GetCreatedAdmins())) - for i, admin := range tt.want.GetCreatedAdmins() { - gotAdmin := got.GetCreatedAdmins()[i] - assertCreatedAdmin(t, admin, gotAdmin) + noOfOrgs := 3 + orgs, orgsName, err := createOrgs(listOrgIAmOwnerCtx, listOrgClient, noOfOrgs) + if err != nil { + require.NoError(t, err) + return + } + + // deactivat org[1] + _, err = listOrgClient.DeactivateOrganization(listOrgIAmOwnerCtx, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgs[1].Id, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + query []*v2beta_org.OrganizationSearchFilter + want []*v2beta_org.Organization + err error + }{ + { + name: "list organizations, without required permissions", + ctx: ListOrgIinstance.WithAuthorization(CTX, integration.UserTypeNoPermission), + err: errors.New("membership not found"), + }, + { + name: "list organizations happy path, no filter", + ctx: listOrgIAmOwnerCtx, + want: []*v2beta_org.Organization{ + { + // default org + Name: "testinstance", + }, + { + Id: orgs[0].Id, + Name: orgsName[0], + }, + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + { + Id: orgs[2].Id, + Name: orgsName[2], + }, + }, + }, + { + name: "list organizations by id happy path", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgs[1].Id, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations by state active", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ + StateFilter: &v2beta_org.OrgStateFilter{ + State: v2beta_org.OrgState_ORG_STATE_ACTIVE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + // default org + Name: "testinstance", + }, + { + Id: orgs[0].Id, + Name: orgsName[0], + }, + { + Id: orgs[2].Id, + Name: orgsName[2], + }, + }, + }, + { + name: "list organizations by state inactive", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ + StateFilter: &v2beta_org.OrgStateFilter{ + State: v2beta_org.OrgState_ORG_STATE_INACTIVE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations by id bad id", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: "bad id", + }, + }, + }, + }, + }, + { + name: "list organizations specify org name equals", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: orgsName[1], + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: func() string { + return orgsName[1][1 : len(orgsName[1])-2] + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: func() string { + return strings.ToUpper(orgsName[1][1 : len(orgsName[1])-2]) + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify domain name equals", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ + Domain: func() string { + listOrgRes, err := listOrgClient.ListOrganizations(listOrgIAmOwnerCtx, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgs[1].Id, + }, + }, + }, + }, + }) + require.NoError(t, err) + domain := listOrgRes.Organizations[0].PrimaryDomain + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify domain name contains", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ + Domain: func() string { + domain := strings.ToLower(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ + Domain: func() string { + domain := strings.ToUpper(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := listOrgClient.ListOrganizations(tt.ctx, &v2beta_org.ListOrganizationsRequest{ + Filter: tt.query, + }) + if tt.err != nil { + require.ErrorContains(t, err, tt.err.Error()) + return + } + require.NoError(ttt, err) + + require.Equal(ttt, uint64(len(tt.want)), got.Pagination.GetTotalResult()) + + foundOrgs := 0 + for _, got := range got.Organizations { + for _, org := range tt.want { + + // created/chagned date + gotCD := got.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) + gotCD = got.GetChangedDate().AsTime() + assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) + + // default org + if org.Name == got.Name && got.Name == "testinstance" { + foundOrgs += 1 + continue + } + + if org.Name == got.Name && + org.Id == got.Id { + foundOrgs += 1 + } + } + } + require.Equal(ttt, len(tt.want), foundOrgs) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_DeleteOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + createOrgFunc func() string + req *v2beta_org.DeleteOrganizationRequest + want *v2beta_org.DeleteOrganizationResponse + dontCheckTime bool + err error + }{ + { + name: "delete org no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + err: errors.New("membership not found"), + }, + { + name: "delete org happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + }, + { + name: "delete already deleted org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + // delete org + _, err = Client.DeleteOrganization(CTX, &v2beta_org.DeleteOrganizationRequest{Id: orgs[0].Id}) + require.NoError(t, err) + + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + dontCheckTime: true, + }, + { + name: "delete non existent org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.DeleteOrganizationRequest{ + Id: "non existent org id", + }, + dontCheckTime: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.createOrgFunc != nil { + tt.req.Id = tt.createOrgFunc() + } + + got, err := Client.DeleteOrganization(tt.ctx, tt.req) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetDeletionDate().AsTime() + if !tt.dontCheckTime { + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) } }) } } -func assertCreatedAdmin(t *testing.T, expected, got *org.AddOrganizationResponse_CreatedAdmin) { +func TestServer_DeactivateReactivateNonExistentOrganization(t *testing.T) { + ctx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + // deactivate non existent organization + _, err := Client.DeactivateOrganization(ctx, &v2beta_org.DeactivateOrganizationRequest{ + Id: "non existent organization", + }) + require.Contains(t, err.Error(), "Organisation not found") + + // reactivate non existent organization + _, err = Client.ActivateOrganization(ctx, &v2beta_org.ActivateOrganizationRequest{ + Id: "non existent organization", + }) + require.Contains(t, err.Error(), "Organisation not found") +} + +func TestServer_ActivateOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + testFunc func() string + err error + }{ + { + name: "Activate, happy path", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + // 2. deactivate organization once + deactivate_res, err := Client.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + gotCD := deactivate_res.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. check organization state is deactivated + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgRes, err := Client.ListOrganizations(CTX, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgId, + }, + }, + }, + }, + }) + require.NoError(ttt, err) + require.Equal(ttt, v2beta_org.OrgState_ORG_STATE_INACTIVE, listOrgRes.Organizations[0].State) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "Activate, no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + // BUG: this needs changing + err: errors.New("membership not found"), + }, + { + name: "Activate, not existing", + ctx: CTX, + testFunc: func() string { + return "non-existing-org-id" + }, + err: errors.New("Organisation not found"), + }, + { + name: "Activate, already activated", + ctx: CTX, + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + err: errors.New("Organisation is already active"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var orgId string + if tt.testFunc != nil { + orgId = tt.testFunc() + } + _, err := Client.ActivateOrganization(tt.ctx, &v2beta_org.ActivateOrganizationRequest{ + Id: orgId, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestServer_DeactivateOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + testFunc func() string + err error + }{ + { + name: "Deactivate, happy path", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + return orgId + }, + }, + { + name: "Deactivate, no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + // BUG: this needs changing + err: errors.New("membership not found"), + }, + { + name: "Deactivate, not existing", + ctx: CTX, + testFunc: func() string { + return "non-existing-org-id" + }, + err: errors.New("Organisation not found"), + }, + { + name: "Deactivate, already deactivated", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + // 2. deactivate organization once + deactivate_res, err := Client.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + gotCD := deactivate_res.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. check organization state is deactivated + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgRes, err := Client.ListOrganizations(CTX, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgId, + }, + }, + }, + }, + }) + require.NoError(ttt, err) + require.Equal(ttt, v2beta_org.OrgState_ORG_STATE_INACTIVE, listOrgRes.Organizations[0].State) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + err: errors.New("Organisation is already deactivated"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var orgId string + orgId = tt.testFunc() + _, err := Client.DeactivateOrganization(tt.ctx, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgId, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestServer_AddOrganizationDomain(t *testing.T) { + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "add org domain, happy path", + domain: gofakeit.URL(), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + return orgId + }, + }, + { + name: "add org domain, twice", + domain: gofakeit.URL(), + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "add org domain to non existent org", + domain: gofakeit.URL(), + testFunc: func() string { + return "non-existing-org-id" + }, + // BUG: should return a error + err: nil, + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: tt.domain, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + } + } +} + +func TestServer_ListOrganizationDomains(t *testing.T) { + domain := gofakeit.URL() + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "list org domain, happy path", + domain: domain, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + return orgId + }, + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + + var err error + var queryRes *v2beta_org.ListOrganizationDomainsResponse + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err = Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == tt.domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for adding domain") + + } +} + +func TestServer_DeleteOerganizationDomain(t *testing.T) { + domain := gofakeit.URL() + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "delete org domain, happy path", + domain: domain, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "delete org domain, twice", + domain: gofakeit.URL(), + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + _, err = Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + return orgId + }, + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "delete org domain to non existent org", + domain: gofakeit.URL(), + testFunc: func() string { + return "non-existing-org-id" + }, + // BUG: + err: errors.New("Domain doesn't exist on organization"), + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + + _, err := Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: tt.domain, + }) + + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + } +} + +func TestServer_AddListDeleteOrganizationDomain(t *testing.T) { + tests := []struct { + name string + testFunc func() + }{ + { + name: "add org domain, re-add org domain", + testFunc: func() { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + // ctx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. re-add domain + _, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + // TODO remove error for adding already existing domain + // require.NoError(t, err) + require.Contains(t, err.Error(), "Errors.Already.Exists") + // check details + // gotCD = addOrgDomainRes.GetDetails().GetChangeDate().AsTime() + // now = time.Now() + // assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 4. check domain is added + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, + }, + { + name: "add org domain, delete org domain, re-delete org domain", + testFunc: func() { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 2. delete organisation domain + deleteOrgDomainRes, err := Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD = deleteOrgDomainRes.GetDeletionDate().AsTime() + now = time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(t *assert.CollectT) { + // 3. check organization domain deleted + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.False(t, found, "deleted domain found") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + // 4. redelete organisation domain + _, err = Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + // TODO remove error for deleting org domain already deleted + // require.NoError(t, err) + require.Contains(t, err.Error(), "Domain doesn't exist on organization") + // check details + // gotCD = deleteOrgDomainRes.GetDetails().GetChangeDate().AsTime() + // now = time.Now() + // assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 5. check organization domain deleted + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.False(t, found, "deleted domain found") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.testFunc() + }) + } +} + +func TestServer_ValidateOrganizationDomain(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + _, err = Instance.Client.Admin.UpdateDomainPolicy(CTX, &admin.UpdateDomainPolicyRequest{ + ValidateOrgDomains: true, + }) + if err != nil && !strings.Contains(err.Error(), "Organisation is already deactivated") { + require.NoError(t, err) + } + + domain := gofakeit.URL() + _, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + req *v2beta_org.GenerateOrganizationDomainValidationRequest + err error + }{ + { + name: "validate org http happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + }, + { + name: "validate org http non existnetn org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: "non existent org id", + Domain: domain, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + // BUG: this should be 'organization does not exist' + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate org dns happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + }, + }, + { + name: "validate org dns non existnetn org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: "non existent org id", + Domain: domain, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + }, + // BUG: this should be 'organization does not exist' + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate org non existnetn domain", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: "non existent domain", + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + err: errors.New("Domain doesn't exist on organization"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.GenerateOrganizationDomainValidation(tt.ctx, tt.req) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + require.NotEmpty(t, got.Token) + require.Contains(t, got.Url, domain) + }) + } +} + +func TestServer_SetOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + key string + value string + err error + }{ + { + name: "set org metadata", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: orgId, + key: "key1", + value: "value1", + }, + { + name: "set org metadata on non existant org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: "non existant orgid", + key: "key2", + value: "value2", + err: errors.New("Organisation not found"), + }, + { + name: "update org metadata", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key3", + Value: []byte("value3"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + key: "key4", + value: "value4", + }, + { + name: "update org metadata with same value", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key5", + Value: []byte("value5"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + key: "key5", + value: "value5", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + got, err := Client.SetOrganizationMetadata(tt.ctx, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: tt.key, + Value: []byte(tt.value), + }, + }, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetSetDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // check metadata + listMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + foundMetadata := false + foundMetadataKeyCount := 0 + for _, res := range listMetadataRes.Metadata { + if res.Key == tt.key { + foundMetadataKeyCount += 1 + } + if res.Key == tt.key && + string(res.Value) == tt.value { + foundMetadata = true + } + } + require.True(ttt, foundMetadata, "unable to find added metadata") + require.Equal(ttt, 1, foundMetadataKeyCount, "same metadata key found multiple times") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_ListOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + keyValuPars []struct { + key string + value string + } + }{ + { + name: "list org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key1", + Value: []byte("value1"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + keyValuPars: []struct{ key, value string }{ + { + key: "key1", + value: "value1", + }, + }, + }, + { + name: "list multiple org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key2", + Value: []byte("value2"), + }, + { + Key: "key3", + Value: []byte("value3"), + }, + { + Key: "key4", + Value: []byte("value4"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + keyValuPars: []struct{ key, value string }{ + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + { + key: "key4", + value: "value4", + }, + }, + }, + { + name: "list org metadata for non existent org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: "non existent orgid", + keyValuPars: []struct{ key, value string }{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(t, err) + + foundMetadataCount := 0 + for _, kv := range tt.keyValuPars { + for _, res := range got.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(t, len(tt.keyValuPars), foundMetadataCount) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_DeleteOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + metadataToDelete []struct { + key string + value string + } + metadataToRemain []struct { + key string + value string + } + err error + }{ + { + name: "delete org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key1", + Value: []byte("value1"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key1", + value: "value1", + }, + }, + }, + { + name: "delete multiple org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key2", + Value: []byte("value2"), + }, + { + Key: "key3", + Value: []byte("value3"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + }, + }, + { + name: "delete some org metadata but not all", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key4", + Value: []byte("value4"), + }, + // key5 should not be deleted + { + Key: "key5", + Value: []byte("value5"), + }, + { + Key: "key6", + Value: []byte("value6"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key4", + value: "value4", + }, + { + key: "key6", + value: "value6", + }, + }, + metadataToRemain: []struct{ key, value string }{ + { + key: "key5", + value: "value5", + }, + }, + }, + { + name: "delete org metadata that does not exist", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key88", + Value: []byte("value74"), + }, + { + Key: "key5888", + Value: []byte("value8885"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + // TODO: this error message needs to be either removed or changed + err: errors.New("Metadata list is empty"), + }, + { + name: "delete org metadata for org that does not exist", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key88", + Value: []byte("value74"), + }, + { + Key: "key5888", + Value: []byte("value8885"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: "non existant org id", + // TODO: this error message needs to be either removed or changed + err: errors.New("Metadata list is empty"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + + // check metadata exists + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(ttt, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToDelete { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(ttt, len(tt.metadataToDelete), foundMetadataCount) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + keys := make([]string, len(tt.metadataToDelete)) + for i, kvp := range tt.metadataToDelete { + keys[i] = kvp.key + } + + // run delete + _, err = Client.DeleteOrganizationMetadata(tt.ctx, &v2beta_org.DeleteOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + Keys: keys, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // check metadata was definitely deleted + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(ttt, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToDelete { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(ttt, foundMetadataCount, 0) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + // check metadata that should not be delted was not deleted + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(t, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToRemain { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(t, len(tt.metadataToRemain), foundMetadataCount) + }) + } +} + +func createOrgs(ctx context.Context, client v2beta_org.OrganizationServiceClient, noOfOrgs int) ([]*v2beta_org.CreateOrganizationResponse, []string, error) { + var err error + orgs := make([]*v2beta_org.CreateOrganizationResponse, noOfOrgs) + orgsName := make([]string, noOfOrgs) + + for i := range noOfOrgs { + orgName := gofakeit.Name() + orgsName[i] = orgName + orgs[i], err = client.CreateOrganization(ctx, + &v2beta_org.CreateOrganizationRequest{ + Name: orgName, + }, + ) + if err != nil { + return nil, nil, err + } + } + + return orgs, orgsName, nil +} + +func assertCreatedAdmin(t *testing.T, expected, got *v2beta_org.CreatedAdmin) { if expected.GetUserId() != "" { assert.NotEmpty(t, got.GetUserId()) } else { diff --git a/internal/api/grpc/org/v2beta/org.go b/internal/api/grpc/org/v2beta/org.go index ab2da2b766..66198757cb 100644 --- a/internal/api/grpc/org/v2beta/org.go +++ b/internal/api/grpc/org/v2beta/org.go @@ -2,16 +2,23 @@ package org import ( "context" + "errors" + "google.golang.org/protobuf/types/known/timestamppb" + + metadata "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2beta" object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" user "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" ) -func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) { - orgSetup, err := addOrganizationRequestToCommand(request) +func (s *Server) CreateOrganization(ctx context.Context, request *v2beta_org.CreateOrganizationRequest) (*v2beta_org.CreateOrganizationResponse, error) { + orgSetup, err := createOrganizationRequestToCommand(request) if err != nil { return nil, err } @@ -22,8 +29,182 @@ func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizati return createdOrganizationToPb(createdOrg) } -func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*command.OrgSetup, error) { - admins, err := addOrganizationRequestAdminsToCommand(request.GetAdmins()) +func (s *Server) UpdateOrganization(ctx context.Context, request *v2beta_org.UpdateOrganizationRequest) (*v2beta_org.UpdateOrganizationResponse, error) { + org, err := s.command.ChangeOrg(ctx, request.Id, request.Name) + if err != nil { + return nil, err + } + + return &v2beta_org.UpdateOrganizationResponse{ + ChangeDate: timestamppb.New(org.EventDate), + }, nil +} + +func (s *Server) ListOrganizations(ctx context.Context, request *v2beta_org.ListOrganizationsRequest) (*v2beta_org.ListOrganizationsResponse, error) { + queries, err := listOrgRequestToModel(s.systemDefaults, request) + if err != nil { + return nil, err + } + orgs, err := s.query.SearchOrgs(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return &v2beta_org.ListOrganizationsResponse{ + Organizations: OrgViewsToPb(orgs.Orgs), + Pagination: &filter.PaginationResponse{ + TotalResult: orgs.Count, + AppliedLimit: uint64(request.GetPagination().GetLimit()), + }, + }, nil +} + +func (s *Server) DeleteOrganization(ctx context.Context, request *v2beta_org.DeleteOrganizationRequest) (*v2beta_org.DeleteOrganizationResponse, error) { + details, err := s.command.RemoveOrg(ctx, request.Id) + if err != nil { + var notFoundError *zerrors.NotFoundError + if errors.As(err, ¬FoundError) { + return &v2beta_org.DeleteOrganizationResponse{}, nil + } + return nil, err + } + return &v2beta_org.DeleteOrganizationResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) SetOrganizationMetadata(ctx context.Context, request *v2beta_org.SetOrganizationMetadataRequest) (*v2beta_org.SetOrganizationMetadataResponse, error) { + result, err := s.command.BulkSetOrgMetadata(ctx, request.OrganizationId, BulkSetOrgMetadataToDomain(request)...) + if err != nil { + return nil, err + } + return &org.SetOrganizationMetadataResponse{ + SetDate: timestamppb.New(result.EventDate), + }, nil +} + +func (s *Server) ListOrganizationMetadata(ctx context.Context, request *v2beta_org.ListOrganizationMetadataRequest) (*v2beta_org.ListOrganizationMetadataResponse, error) { + metadataQueries, err := ListOrgMetadataToDomain(s.systemDefaults, request) + if err != nil { + return nil, err + } + res, err := s.query.SearchOrgMetadata(ctx, true, request.OrganizationId, metadataQueries, false) + if err != nil { + return nil, err + } + return &v2beta_org.ListOrganizationMetadataResponse{ + Metadata: metadata.OrgMetadataListToPb(res.Metadata), + Pagination: &filter.PaginationResponse{ + TotalResult: res.Count, + AppliedLimit: uint64(request.GetPagination().GetLimit()), + }, + }, nil +} + +func (s *Server) DeleteOrganizationMetadata(ctx context.Context, request *v2beta_org.DeleteOrganizationMetadataRequest) (*v2beta_org.DeleteOrganizationMetadataResponse, error) { + result, err := s.command.BulkRemoveOrgMetadata(ctx, request.OrganizationId, request.Keys...) + if err != nil { + return nil, err + } + return &v2beta_org.DeleteOrganizationMetadataResponse{ + DeletionDate: timestamppb.New(result.EventDate), + }, nil +} + +func (s *Server) DeactivateOrganization(ctx context.Context, request *org.DeactivateOrganizationRequest) (*org.DeactivateOrganizationResponse, error) { + objectDetails, err := s.command.DeactivateOrg(ctx, request.Id) + if err != nil { + return nil, err + } + return &org.DeactivateOrganizationResponse{ + ChangeDate: timestamppb.New(objectDetails.EventDate), + }, nil +} + +func (s *Server) ActivateOrganization(ctx context.Context, request *org.ActivateOrganizationRequest) (*org.ActivateOrganizationResponse, error) { + objectDetails, err := s.command.ReactivateOrg(ctx, request.Id) + if err != nil { + return nil, err + } + return &org.ActivateOrganizationResponse{ + ChangeDate: timestamppb.New(objectDetails.EventDate), + }, err +} + +func (s *Server) AddOrganizationDomain(ctx context.Context, request *org.AddOrganizationDomainRequest) (*org.AddOrganizationDomainResponse, error) { + userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, request.Domain, request.OrganizationId) + if err != nil { + return nil, err + } + details, err := s.command.AddOrgDomain(ctx, request.OrganizationId, request.Domain, userIDs) + if err != nil { + return nil, err + } + return &org.AddOrganizationDomainResponse{ + CreationDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) ListOrganizationDomains(ctx context.Context, req *org.ListOrganizationDomainsRequest) (*org.ListOrganizationDomainsResponse, error) { + queries, err := ListOrgDomainsRequestToModel(s.systemDefaults, req) + if err != nil { + return nil, err + } + orgIDQuery, err := query.NewOrgDomainOrgIDSearchQuery(req.OrganizationId) + if err != nil { + return nil, err + } + queries.Queries = append(queries.Queries, orgIDQuery) + + domains, err := s.query.SearchOrgDomains(ctx, queries, false) + if err != nil { + return nil, err + } + return &org.ListOrganizationDomainsResponse{ + Domains: object.DomainsToPb(domains.Domains), + Pagination: &filter.PaginationResponse{ + TotalResult: domains.Count, + AppliedLimit: uint64(req.GetPagination().GetLimit()), + }, + }, nil +} + +func (s *Server) DeleteOrganizationDomain(ctx context.Context, req *org.DeleteOrganizationDomainRequest) (*org.DeleteOrganizationDomainResponse, error) { + details, err := s.command.RemoveOrgDomain(ctx, RemoveOrgDomainRequestToDomain(ctx, req)) + if err != nil { + return nil, err + } + return &org.DeleteOrganizationDomainResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, err +} + +func (s *Server) GenerateOrganizationDomainValidation(ctx context.Context, req *org.GenerateOrganizationDomainValidationRequest) (*org.GenerateOrganizationDomainValidationResponse, error) { + token, url, err := s.command.GenerateOrgDomainValidation(ctx, GenerateOrgDomainValidationRequestToDomain(ctx, req)) + if err != nil { + return nil, err + } + return &org.GenerateOrganizationDomainValidationResponse{ + Token: token, + Url: url, + }, nil +} + +func (s *Server) VerifyOrganizationDomain(ctx context.Context, request *org.VerifyOrganizationDomainRequest) (*org.VerifyOrganizationDomainResponse, error) { + userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, request.Domain, request.OrganizationId) + if err != nil { + return nil, err + } + details, err := s.command.ValidateOrgDomain(ctx, ValidateOrgDomainRequestToDomain(ctx, request), userIDs) + if err != nil { + return nil, err + } + return &org.VerifyOrganizationDomainResponse{ + ChangeDate: timestamppb.New(details.EventDate), + }, nil +} + +func createOrganizationRequestToCommand(request *v2beta_org.CreateOrganizationRequest) (*command.OrgSetup, error) { + admins, err := createOrganizationRequestAdminsToCommand(request.GetAdmins()) if err != nil { return nil, err } @@ -31,13 +212,14 @@ func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*comm Name: request.GetName(), CustomDomain: "", Admins: admins, + OrgID: request.GetId(), }, nil } -func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) { +func createOrganizationRequestAdminsToCommand(requestAdmins []*v2beta_org.CreateOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) { admins = make([]*command.OrgSetupAdmin, len(requestAdmins)) for i, admin := range requestAdmins { - admins[i], err = addOrganizationRequestAdminToCommand(admin) + admins[i], err = createOrganizationRequestAdminToCommand(admin) if err != nil { return nil, err } @@ -45,14 +227,14 @@ func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationR return admins, nil } -func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) { +func createOrganizationRequestAdminToCommand(admin *v2beta_org.CreateOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) { switch a := admin.GetUserType().(type) { - case *org.AddOrganizationRequest_Admin_UserId: + case *v2beta_org.CreateOrganizationRequest_Admin_UserId: return &command.OrgSetupAdmin{ ID: a.UserId, Roles: admin.GetRoles(), }, nil - case *org.AddOrganizationRequest_Admin_Human: + case *v2beta_org.CreateOrganizationRequest_Admin_Human: human, err := user.AddUserRequestToAddHuman(a.Human) if err != nil { return nil, err @@ -62,22 +244,31 @@ func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admi Roles: admin.GetRoles(), }, nil default: - return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", a) + return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SL2r8", "userType oneOf %T in method AddOrganization not implemented", a) } } -func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.AddOrganizationResponse, err error) { - admins := make([]*org.AddOrganizationResponse_CreatedAdmin, len(createdOrg.CreatedAdmins)) - for i, admin := range createdOrg.CreatedAdmins { - admins[i] = &org.AddOrganizationResponse_CreatedAdmin{ - UserId: admin.ID, - EmailCode: admin.EmailCode, - PhoneCode: admin.PhoneCode, - } +func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, orgID string) ([]string, error) { + queries := make([]query.SearchQuery, 0, 2) + loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase) + if err != nil { + return nil, err } - return &org.AddOrganizationResponse{ - Details: object.DomainToDetailsPb(createdOrg.ObjectDetails), - OrganizationId: createdOrg.ObjectDetails.ResourceOwner, - CreatedAdmins: admins, - }, nil + queries = append(queries, loginName) + if orgID != "" { + owner, err := query.NewUserResourceOwnerSearchQuery(orgID, query.TextNotEquals) + if err != nil { + return nil, err + } + queries = append(queries, owner) + } + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}, nil) + if err != nil { + return nil, err + } + userIDs := make([]string, len(users.Users)) + for i, user := range users.Users { + userIDs[i] = user.ID + } + return userIDs, nil } diff --git a/internal/api/grpc/org/v2beta/org_test.go b/internal/api/grpc/org/v2beta/org_test.go index 5024b59c1d..346d6b88c1 100644 --- a/internal/api/grpc/org/v2beta/org_test.go +++ b/internal/api/grpc/org/v2beta/org_test.go @@ -12,14 +12,13 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func Test_addOrganizationRequestToCommand(t *testing.T) { +func Test_createOrganizationRequestToCommand(t *testing.T) { type args struct { - request *org.AddOrganizationRequest + request *org.CreateOrganizationRequest } tests := []struct { name string @@ -30,23 +29,38 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "nil user", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ {}, }, }, }, - wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil), + wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SL2r8", "userType oneOf %T in method AddOrganization not implemented", nil), + }, + { + name: "custom org ID", + args: args{ + request: &org.CreateOrganizationRequest{ + Name: "custom org ID", + Id: gu.Ptr("org-ID"), + }, + }, + want: &command.OrgSetup{ + Name: "custom org ID", + CustomDomain: "", + Admins: []*command.OrgSetupAdmin{}, + OrgID: "org-ID", + }, }, { name: "user ID", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_UserId{ + UserType: &org.CreateOrganizationRequest_Admin_UserId{ UserId: "userID", }, Roles: nil, @@ -67,11 +81,11 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "human user", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &org.CreateOrganizationRequest_Admin_Human{ Human: &user.AddHumanUserRequest{ Profile: &user.SetHumanProfile{ GivenName: "firstname", @@ -109,7 +123,7 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := addOrganizationRequestToCommand(tt.args.request) + got, err := createOrganizationRequestToCommand(tt.args.request) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) @@ -124,7 +138,7 @@ func Test_createdOrganizationToPb(t *testing.T) { tests := []struct { name string args args - want *org.AddOrganizationResponse + want *org.CreateOrganizationResponse wantErr error }{ { @@ -136,8 +150,8 @@ func Test_createdOrganizationToPb(t *testing.T) { EventDate: now, ResourceOwner: "orgID", }, - CreatedAdmins: []*command.CreatedOrgAdmin{ - { + OrgAdmins: []command.OrgAdmin{ + &command.CreatedOrgAdmin{ ID: "id", EmailCode: gu.Ptr("emailCode"), PhoneCode: gu.Ptr("phoneCode"), @@ -145,18 +159,18 @@ func Test_createdOrganizationToPb(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - Details: &object.Details{ - Sequence: 1, - ChangeDate: timestamppb.New(now), - ResourceOwner: "orgID", - }, - OrganizationId: "orgID", - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: &org.CreateOrganizationResponse{ + CreationDate: timestamppb.New(now), + Id: "orgID", + OrganizationAdmins: []*org.OrganizationAdmin{ { - UserId: "id", - EmailCode: gu.Ptr("emailCode"), - PhoneCode: gu.Ptr("phoneCode"), + OrganizationAdmin: &org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &org.CreatedAdmin{ + UserId: "id", + EmailCode: gu.Ptr("emailCode"), + PhoneCode: gu.Ptr("phoneCode"), + }, + }, }, }, }, diff --git a/internal/api/grpc/org/v2beta/server.go b/internal/api/grpc/org/v2beta/server.go index 89dba81702..b7e8d4994f 100644 --- a/internal/api/grpc/org/v2beta/server.go +++ b/internal/api/grpc/org/v2beta/server.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" @@ -15,6 +16,7 @@ var _ org.OrganizationServiceServer = (*Server)(nil) type Server struct { org.UnimplementedOrganizationServiceServer + systemDefaults systemdefaults.SystemDefaults command *command.Commands query *query.Queries checkPermission domain.PermissionCheck @@ -23,11 +25,13 @@ type Server struct { type Config struct{} func CreateServer( + systemDefaults systemdefaults.SystemDefaults, command *command.Commands, query *query.Queries, checkPermission domain.PermissionCheck, ) *Server { return &Server{ + systemDefaults: systemDefaults, command: command, query: query, checkPermission: checkPermission, diff --git a/internal/api/grpc/project/v2beta/integration/project_grant_test.go b/internal/api/grpc/project/v2beta/integration/project_grant_test.go new file mode 100644 index 0000000000..34fa10e3de --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration/project_grant_test.go @@ -0,0 +1,1351 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_CreateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.CreateProjectGrantRequest) + req *project.CreateProjectGrantRequest + want + wantErr bool + }{ + { + name: "empty projectID", + ctx: iamOwnerCtx, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "empty granted organization", + ctx: iamOwnerCtx, + req: &project.CreateProjectGrantRequest{ + ProjectId: "something", + GrantedOrganizationId: "", + }, + wantErr: true, + }, + { + name: "project not existing", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + request.ProjectId = "something" + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "org not existing", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + request.GrantedOrganizationId = "something" + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "same organization, error", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + request.GrantedOrganizationId = orgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "empty, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + { + name: "with roles, not existing", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + roles := []string{gofakeit.Name(), gofakeit.Name(), gofakeit.Name()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + request.RoleKeys = roles + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "with roles, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + roles := []string{gofakeit.Name(), gofakeit.Name(), gofakeit.Name()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + request.RoleKeys = roles + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.CreateProjectGrant(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectGrantResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func TestServer_CreateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.CreateProjectGrantRequest) + req *project.CreateProjectGrantRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "project owner, other project", + ctx: projectOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + { + name: "instance owner", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.CreateProjectGrant(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectGrantResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertCreateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate bool, actualResp *project.CreateProjectGrantResponse) { + 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) + } +} + +func TestServer_UpdateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.UpdateProjectGrantRequest) { + request.ProjectId = "notexisting" + request.GrantedOrganizationId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"notexisting"}, + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change roles, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId(), roles...) + request.RoleKeys = roles + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "change roles, not existing", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + request.RoleKeys = []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(tt.args.req) + } + + got, err := instance.Client.Projectv2Beta.UpdateProjectGrant(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) + assertUpdateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectGrant(iamOwnerCtx, t, projectID, orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectID, orgResp.GetOrganizationId(), userResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + type args struct { + ctx context.Context + req *project.UpdateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: CTX, + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"nopermission"}, + }, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"nopermission"}, + }, + }, + wantErr: true, + }, + { + name: "project grant owner, no permission", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + request.ProjectId = projectID + request.GrantedOrganizationId = orgResp.GetOrganizationId() + + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectID, role, role, "") + } + + request.RoleKeys = roles + }, + args: args{ + ctx: projectGrantOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"nopermission"}, + }, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId(), roles...) + request.RoleKeys = roles + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId(), roles...) + request.RoleKeys = roles + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(tt.args.req) + } + + got, err := instance.Client.Projectv2Beta.UpdateProjectGrant(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) + assertUpdateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertUpdateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.UpdateProjectGrantResponse) { + 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) + } +} + +func TestServer_DeleteProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) + req *project.DeleteProjectGrantRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty project id", + ctx: iamOwnerCtx, + req: &project.DeleteProjectGrantRequest{ + ProjectId: "", + }, + wantErr: true, + }, + { + name: "empty grantedorg id", + ctx: iamOwnerCtx, + req: &project.DeleteProjectGrantRequest{ + ProjectId: "notempty", + GrantedOrganizationId: "", + }, + wantErr: true, + }, + { + name: "delete, not existing", + ctx: iamOwnerCtx, + req: &project.DeleteProjectGrantRequest{ + ProjectId: "notexisting", + GrantedOrganizationId: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + { + name: "delete deactivated", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeleteProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Now().UTC() + }, + req: &project.DeleteProjectGrantRequest{}, + 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.Projectv2Beta.DeleteProjectGrant(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectGrantResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) + req *project.DeleteProjectGrantRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return time.Time{}, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return time.Time{}, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + { + name: "organization owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + 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.Projectv2Beta.DeleteProjectGrant(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectGrantResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteProjectGrantResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *project.DeleteProjectGrantResponse) { + 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) + } +} + +func TestServer_DeactivateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *project.DeactivateProjectGrantRequest) { + request.ProjectId = "notexisting" + request.GrantedOrganizationId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + 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.Projectv2Beta.DeactivateProjectGrant(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) + assertDeactivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_DeactivateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: CTX, + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + 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.Projectv2Beta.DeactivateProjectGrant(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) + assertDeactivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertDeactivateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.DeactivateProjectGrantResponse) { + 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) + } +} + +func TestServer_ActivateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.ActivateProjectGrantRequest) { + request.ProjectId = "notexisting" + request.GrantedOrganizationId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + 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.Projectv2Beta.ActivateProjectGrant(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) + assertActivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_ActivateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: CTX, + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + 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.Projectv2Beta.ActivateProjectGrant(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) + assertActivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertActivateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.ActivateProjectGrantResponse) { + 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) + } +} diff --git a/internal/api/grpc/project/v2beta/integration/project_role_test.go b/internal/api/grpc/project/v2beta/integration/project_role_test.go new file mode 100644 index 0000000000..5e2f0e447e --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration/project_role_test.go @@ -0,0 +1,698 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_AddProjectRole(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + alreadyExistingProject := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + alreadyExistingProjectRoleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, alreadyExistingProject.GetId(), alreadyExistingProjectRoleName, alreadyExistingProjectRoleName, "") + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.AddProjectRoleRequest) + req *project.AddProjectRoleRequest + want + wantErr bool + }{ + { + name: "empty key", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: "", + DisplayName: gofakeit.AppName(), + }, + wantErr: true, + }, + { + name: "empty displayname", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: "", + }, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + request.ProjectId = alreadyExistingProject.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: alreadyExistingProjectRoleName, + DisplayName: alreadyExistingProjectRoleName, + }, + wantErr: true, + }, + { + name: "empty, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.Name(), + DisplayName: gofakeit.Name(), + }, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.AddProjectRole(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertAddProjectRoleResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func TestServer_AddProjectRole_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + alreadyExistingProject := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + alreadyExistingProjectRoleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, alreadyExistingProject.GetId(), alreadyExistingProjectRoleName, alreadyExistingProjectRoleName, "") + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.AddProjectRoleRequest) + req *project.AddProjectRoleRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + want: want{ + creationDate: true, + }, + }, + { + name: "instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.AddProjectRole(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertAddProjectRoleResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertAddProjectRoleResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate bool, actualResp *project.AddProjectRoleResponse) { + 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) + } +} + +func TestServer_UpdateProjectRole(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRoleRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRoleRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *project.UpdateProjectRoleRequest) { + request.RoleKey = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + request.DisplayName = gu.Ptr(roleName) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change display name, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "change full, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(gofakeit.AppName()), + Group: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + 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.Projectv2Beta.UpdateProjectRole(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) + assertUpdateProjectRoleResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateProjectRole_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRoleRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRoleRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenicated", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: CTX, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + 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.Projectv2Beta.UpdateProjectRole(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) + assertUpdateProjectRoleResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertUpdateProjectRoleResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.UpdateProjectRoleResponse) { + 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) + } +} + +func TestServer_DeleteProjectRole(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) + req *project.RemoveProjectRoleRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty id", + ctx: iamOwnerCtx, + req: &project.RemoveProjectRoleRequest{ + ProjectId: "", + RoleKey: "notexisting", + }, + wantErr: true, + }, + { + name: "delete, not existing", + ctx: iamOwnerCtx, + req: &project.RemoveProjectRoleRequest{ + ProjectId: "notexisting", + RoleKey: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete", + ctx: iamOwnerCtx, + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCtx, + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + instance.RemoveProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName) + return creationDate, time.Now().UTC() + }, + req: &project.RemoveProjectRoleRequest{}, + 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.Projectv2Beta.RemoveProjectRole(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertRemoveProjectRoleResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteProjectRole_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) + req *project.RemoveProjectRoleRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + { + name: "instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + 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.Projectv2Beta.RemoveProjectRole(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertRemoveProjectRoleResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertRemoveProjectRoleResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *project.RemoveProjectRoleResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetRemovalDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetRemovalDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.RemovalDate) + } +} diff --git a/internal/api/grpc/project/v2beta/integration/project_test.go b/internal/api/grpc/project/v2beta/integration/project_test.go new file mode 100644 index 0000000000..5412f6eb58 --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration/project_test.go @@ -0,0 +1,1078 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_CreateProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + alreadyExistingProjectName := gofakeit.AppName() + instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), alreadyExistingProjectName, false, false) + + type want struct { + id bool + creationDate bool + } + tests := []struct { + name string + ctx context.Context + req *project.CreateProjectRequest + want + wantErr bool + }{ + { + name: "empty name", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: "", + }, + wantErr: true, + }, + { + name: "empty organization", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: "", + }, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: alreadyExistingProjectName, + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "empty, ok", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.CreateProject(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, got) + }) + } +} + +func TestServer_CreateProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type want struct { + id bool + creationDate bool + } + tests := []struct { + name string + ctx context.Context + req *project.CreateProjectRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "missing permission, other organization", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: instance.DefaultOrg.GetId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + { + name: "instance owner, ok", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.CreateProject(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, got) + }) + } +} + +func assertCreateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate, expectedID bool, actualResp *project.CreateProjectResponse) { + 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) + } +} + +func TestServer_UpdateProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.UpdateProjectRequest) { + request.Id = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.UpdateProjectRequest) { + name := gofakeit.AppName() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false).GetId() + request.Id = projectID + request.Name = gu.Ptr(name) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change name, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "change full, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + ProjectRoleAssertion: gu.Ptr(true), + ProjectRoleCheck: gu.Ptr(true), + HasProjectCheck: gu.Ptr(true), + PrivateLabelingSetting: gu.Ptr(project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + 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.Projectv2Beta.UpdateProject(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) + assertUpdateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectMembership(t, iamOwnerCtx, projectID, userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + type args struct { + ctx context.Context + req *project.UpdateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.UpdateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: CTX, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "missing permission", + prepare: func(request *project.UpdateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "project owner, no permission", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: projectOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + wantErr: true, + }, + { + name: " roject owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + request.Id = projectID + }, + args: args{ + ctx: projectOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "missing permission, other organization", + prepare: func(request *project.UpdateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + 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.Projectv2Beta.UpdateProject(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) + assertUpdateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertUpdateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.UpdateProjectResponse) { + 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) + } +} + +func TestServer_DeleteProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectRequest) (time.Time, time.Time) + req *project.DeleteProjectRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty id", + ctx: iamOwnerCtx, + req: &project.DeleteProjectRequest{ + Id: "", + }, + wantErr: true, + }, + { + name: "delete, not existing", + ctx: iamOwnerCtx, + req: &project.DeleteProjectRequest{ + Id: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + instance.DeleteProject(iamOwnerCtx, t, projectID) + return creationDate, time.Now().UTC() + }, + req: &project.DeleteProjectRequest{}, + 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.Projectv2Beta.DeleteProject(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectMembership(t, iamOwnerCtx, projectID, userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectRequest) (time.Time, time.Time) + req *project.DeleteProjectRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "project owner, no permission", + ctx: projectOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "organization owner", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + { + name: "instance owner", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + 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.Projectv2Beta.DeleteProject(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteProjectResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *project.DeleteProjectResponse) { + 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) + } +} + +func TestServer_DeactivateProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.DeactivateProjectRequest) { + request.Id = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.DeactivateProjectRequest) { + name := gofakeit.AppName() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false).GetId() + request.Id = projectID + instance.DeactivateProject(iamOwnerCtx, t, projectID) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "change, ok", + prepare: func(request *project.DeactivateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + 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.Projectv2Beta.DeactivateProject(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) + assertDeactivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_DeactivateProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: CTX, + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "missing permission", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + 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.Projectv2Beta.DeactivateProject(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) + assertDeactivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertDeactivateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.DeactivateProjectResponse) { + 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) + } +} + +func TestServer_ActivateProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *project.ActivateProjectRequest) { + request.Id = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.ActivateProjectRequest) { + name := gofakeit.AppName() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "change, ok", + prepare: func(request *project.ActivateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + instance.DeactivateProject(iamOwnerCtx, t, projectID) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + 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.Projectv2Beta.ActivateProject(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) + assertActivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_ActivateProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: CTX, + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "missing permission", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + 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.Projectv2Beta.ActivateProject(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) + assertActivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertActivateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.ActivateProjectResponse) { + 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) + } +} diff --git a/internal/api/grpc/project/v2beta/integration/query_test.go b/internal/api/grpc/project/v2beta/integration/query_test.go new file mode 100644 index 0000000000..b648e8c1d7 --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration/query_test.go @@ -0,0 +1,1906 @@ +//go:build integration + +package project_test + +import ( + "context" + "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/integration" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_GetProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*project.GetProjectRequest, *project.GetProjectResponse) + req *project.GetProjectRequest + } + tests := []struct { + name string + args args + want *project.GetProjectResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, false, false) + + request.Id = resp.GetId() + }, + req: &project.GetProjectRequest{}, + }, + wantErr: true, + }, + { + name: "missing permission, other org owner", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + name := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + + request.Id = resp.GetId() + }, + req: &project.GetProjectRequest{}, + }, + wantErr: true, + }, + { + name: "not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.GetProjectRequest{Id: "notexisting"}, + }, + wantErr: true, + }, + { + name: "get, ok", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, false, false) + + request.Id = resp.GetId() + response.Project = resp + }, + req: &project.GetProjectRequest{}, + }, + want: &project.GetProjectResponse{ + Project: &project.Project{ + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: false, + AuthorizationRequired: false, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + }, + }, + }, + { + name: "get, ok, org owner", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, false, false) + + request.Id = resp.GetId() + response.Project = resp + }, + req: &project.GetProjectRequest{}, + }, + want: &project.GetProjectResponse{ + Project: &project.Project{}, + }, + }, + { + name: "get, full, ok", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + + request.Id = resp.GetId() + response.Project = resp + }, + req: &project.GetProjectRequest{}, + }, + want: &project.GetProjectResponse{ + Project: &project.Project{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, 2*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.Projectv2Beta.GetProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, err) + return + } + assert.NoError(ttt, err) + assert.EqualExportedValues(ttt, tt.want, got) + }, retryDuration, tick, "timeout waiting for expected project result") + }) + } +} + +func TestServer_ListProjects(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetUserId()) + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + type args struct { + ctx context.Context + dep func(*project.ListProjectsRequest, *project.ListProjectsResponse) + req *project.ListProjectsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + resp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + resp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + name := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{ + {Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list single name", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectNameFilter{ + ProjectNameFilter: &project.ProjectNameFilter{ + ProjectName: response.Projects[0].Name, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + { + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: false, + AuthorizationRequired: false, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + }, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + response.Projects[2] = createProject(iamOwnerCtx, instance, t, orgID, false, false) + response.Projects[1] = createProject(iamOwnerCtx, instance, t, orgID, true, false) + response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp1 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, false) + resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) + resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + }, + } + + response.Projects[0] = resp2 + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp1 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, false) + resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) + resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}}, + } + response.Projects[0] = grantedProjectResp + response.Projects[1] = projectResp + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 5, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + response.Projects[3] = projectResp + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + response.Projects[2] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[1] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[0] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, organization", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[1] = grantedProjectResp + response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectOrganizationIdFilter{ + ProjectOrganizationIdFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, project resourceowner", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectResourceOwnerFilter{ + ProjectResourceOwnerFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list granted project, project id", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + + orgName := gofakeit.AppName() + projectName := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, orgName, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), projectName, true, true) + projectGrantResp := instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{projectResp.GetId()}}, + } + response.Projects[0] = &project.Project{ + Id: projectResp.GetId(), + Name: projectName, + OrganizationId: orgResp.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: true, + AuthorizationRequired: true, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(orgID), + GrantedOrganizationName: gu.Ptr(instance.DefaultOrg.GetName()), + GrantedState: 1, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.Projectv2Beta.ListProjects(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Projects, len(tt.want.Projects)) { + for i := range tt.want.Projects { + assert.EqualExportedValues(ttt, tt.want.Projects[i], got.Projects[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListProjects_PermissionV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgID := instancePermissionV2.DefaultOrg.GetId() + + type args struct { + ctx context.Context + dep func(*project.ListProjectsRequest, *project.ListProjectsResponse) + req *project.ListProjectsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Projects: []*project.Project{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{ + {Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list single name", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectNameFilter{ + ProjectNameFilter: &project.ProjectNameFilter{ + ProjectName: response.Projects[0].Name, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + response.Projects[2] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + response.Projects[1] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false) + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) + response.Projects[3] = projectResp + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + response.Projects[2] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[1] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[0] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, organization", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[1] = grantedProjectResp + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectOrganizationIdFilter{ + ProjectOrganizationIdFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, project resourceowner", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectResourceOwnerFilter{ + ProjectResourceOwnerFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp1 := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, false) + resp2 := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false) + resp3 := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + }, + } + + response.Projects[0] = resp2 + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + // TODO: correct when permission check is added for project grants https://github.com/zitadel/zitadel/issues/9972 + { + name: "list granted project, project id", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + + orgName := gofakeit.AppName() + projectName := gofakeit.AppName() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, orgName, gofakeit.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), projectName, true, true) + // projectGrantResp := + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{projectResp.GetId()}}, + } + /* + response.Projects[0] = &project.Project{ + Id: projectResp.GetId(), + Name: projectName, + OrganizationId: orgResp.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: true, + AuthorizationRequired: true, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(orgID), + GrantedOrganizationName: gu.Ptr(instancePermissionV2.DefaultOrg.GetName()), + GrantedState: 1, + } + */ + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Projects: []*project.Project{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.Projectv2Beta.ListProjects(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Projects, len(tt.want.Projects)) { + for i := range tt.want.Projects { + assert.EqualExportedValues(ttt, tt.want.Projects[i], got.Projects[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func createProject(ctx context.Context, instance *integration.Instance, t *testing.T, orgID string, projectRoleCheck, hasProjectCheck bool) *project.Project { + name := gofakeit.AppName() + resp := instance.CreateProject(ctx, t, orgID, name, projectRoleCheck, hasProjectCheck) + return &project.Project{ + Id: resp.GetId(), + Name: name, + OrganizationId: orgID, + CreationDate: resp.GetCreationDate(), + ChangeDate: resp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: hasProjectCheck, + AuthorizationRequired: projectRoleCheck, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + } +} + +func createGrantedProject(ctx context.Context, instance *integration.Instance, t *testing.T, projectToGrant *project.Project) *project.Project { + grantedOrgName := gofakeit.AppName() + grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, gofakeit.Email()) + projectGrantResp := instance.CreateProjectGrant(ctx, t, projectToGrant.GetId(), grantedOrg.GetOrganizationId()) + + return &project.Project{ + Id: projectToGrant.GetId(), + Name: projectToGrant.GetName(), + OrganizationId: projectToGrant.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: projectToGrant.GetProjectAccessRequired(), + AuthorizationRequired: projectToGrant.GetAuthorizationRequired(), + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(grantedOrg.GetOrganizationId()), + GrantedOrganizationName: gu.Ptr(grantedOrgName), + GrantedState: 1, + } +} + +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_ListProjectGrants(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + projectGrantResp := createProjectGrant(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectResp.GetName()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), projectGrantResp.GetGrantedOrganizationId(), userResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + type args struct { + ctx context.Context + dep func(*project.ListProjectGrantsRequest, *project.ListProjectGrantsResponse) + req *project.ListProjectGrantsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectGrantsResponse + wantErr bool + }{{ + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{ + {Filter: &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list by id", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{}, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[2] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[1] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := gofakeit.AppName() + name2 := gofakeit.AppName() + name3 := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + project1Resp := instance.CreateProject(iamOwnerCtx, t, orgID, name1, false, false) + project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) + project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, project1Resp.GetId(), name1) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project2Resp.GetId(), name2) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project3Resp.GetId(), name3) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := gofakeit.AppName() + name2 := gofakeit.AppName() + name3 := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + project1Resp := instance.CreateProject(iamOwnerCtx, t, orgID, name1, false, false) + project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) + project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}}, + } + + createProjectGrant(iamOwnerCtx, instance, t, orgID, project1Resp.GetId(), name1) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project2Resp.GetId(), name2) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project3Resp.GetId(), name3) + response.ProjectGrants[0] = projectGrantResp + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list single id with role", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name, projectRoleResp.Key) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list single id with removed role", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name, projectRoleResp.Key) + + removeRoleResp := instance.RemoveProjectRole(iamOwnerCtx, t, projectResp.GetId(), projectRoleResp.Key) + response.ProjectGrants[0].GrantedRoleKeys = nil + response.ProjectGrants[0].ChangeDate = removeRoleResp.GetRemovalDate() + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.Projectv2Beta.ListProjectGrants(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.ProjectGrants, len(tt.want.ProjectGrants)) { + for i := range tt.want.ProjectGrants { + assert.EqualExportedValues(ttt, tt.want.ProjectGrants[i], got.ProjectGrants[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*project.ListProjectGrantsRequest, *project.ListProjectGrantsResponse) + req *project.ListProjectGrantsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectGrantsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, + } + + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, + } + + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list by id", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{}, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[2] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[1] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := gofakeit.AppName() + name2 := gofakeit.AppName() + name3 := gofakeit.AppName() + orgID := instancePermissionV2.DefaultOrg.GetId() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + project1Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name1, false, false) + project2Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) + project3Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, project1Resp.GetId(), name1) + createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), project2Resp.GetId(), name2) + createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), project3Resp.GetId(), name3) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.Projectv2Beta.ListProjectGrants(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.ProjectGrants, len(tt.want.ProjectGrants)) { + for i := range tt.want.ProjectGrants { + assert.EqualExportedValues(ttt, tt.want.ProjectGrants[i], got.ProjectGrants[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func createProjectGrant(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, projectID, projectName string, roles ...string) *project.ProjectGrant { + grantedOrgName := gofakeit.AppName() + grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, gofakeit.Email()) + projectGrantResp := instance.CreateProjectGrant(ctx, t, projectID, grantedOrg.GetOrganizationId(), roles...) + + return &project.ProjectGrant{ + OrganizationId: orgID, + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + GrantedOrganizationId: grantedOrg.GetOrganizationId(), + GrantedOrganizationName: grantedOrgName, + ProjectId: projectID, + ProjectName: projectName, + State: 1, + GrantedRoleKeys: roles, + } +} + +func TestServer_ListProjectRoles(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + dep func(*project.ListProjectRolesRequest, *project.ListProjectRolesResponse) + req *project.ListProjectRolesRequest + } + tests := []struct { + name string + args args + want *project.ListProjectRolesResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectRolesRequest{ + ProjectId: "notfound", + }, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, missing permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[2] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectRoles[1] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, {}, {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.Projectv2Beta.ListProjectRoles(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.ProjectRoles, len(tt.want.ProjectRoles)) { + for i := range tt.want.ProjectRoles { + assert.EqualExportedValues(ttt, tt.want.ProjectRoles[i], got.ProjectRoles[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListProjectRoles_PermissionV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*project.ListProjectRolesRequest, *project.ListProjectRolesResponse) + req *project.ListProjectRolesRequest + } + tests := []struct { + name string + args args + want *project.ListProjectRolesResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectRolesRequest{ + ProjectId: "notfound", + }, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[2] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + response.ProjectRoles[1] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, {}, {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.Projectv2Beta.ListProjectRoles(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.ProjectRoles, len(tt.want.ProjectRoles)) { + for i := range tt.want.ProjectRoles { + assert.EqualExportedValues(ttt, tt.want.ProjectRoles[i], got.ProjectRoles[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func addProjectRole(ctx context.Context, instance *integration.Instance, t *testing.T, projectID string) *project.ProjectRole { + name := gofakeit.Animal() + projectRoleResp := instance.AddProjectRole(ctx, t, projectID, name, name, name) + + return &project.ProjectRole{ + ProjectId: projectID, + CreationDate: projectRoleResp.GetCreationDate(), + ChangeDate: projectRoleResp.GetCreationDate(), + Key: name, + DisplayName: name, + Group: name, + } +} diff --git a/internal/api/grpc/project/v2beta/integration/server_test.go b/internal/api/grpc/project/v2beta/integration/server_test.go new file mode 100644 index 0000000000..59d9745222 --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration/server_test.go @@ -0,0 +1,63 @@ +//go:build integration + +package project_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" +) + +var ( + CTX context.Context + instance *integration.Instance + instancePermissionV2 *integration.Instance +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + CTX = ctx + instance = integration.NewInstance(ctx) + instancePermissionV2 = integration.NewInstance(CTX) + return m.Run() + }()) +} + +func ensureFeaturePermissionV2Enabled(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.PermissionCheckV2.GetEnabled() { + return + } + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: 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) + if f.PermissionCheckV2.GetEnabled() { + return + } + }, + retryDuration, + tick, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/project/v2beta/project.go b/internal/api/grpc/project/v2beta/project.go new file mode 100644 index 0000000000..01b478f5be --- /dev/null +++ b/internal/api/grpc/project/v2beta/project.go @@ -0,0 +1,161 @@ +package project + +import ( + "context" + + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func (s *Server) CreateProject(ctx context.Context, req *project_pb.CreateProjectRequest) (*project_pb.CreateProjectResponse, error) { + add := projectCreateToCommand(req) + project, err := s.command.AddProject(ctx, add) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + creationDate = timestamppb.New(project.EventDate) + } + return &project_pb.CreateProjectResponse{ + Id: add.AggregateID, + CreationDate: creationDate, + }, nil +} + +func projectCreateToCommand(req *project_pb.CreateProjectRequest) *command.AddProject { + var aggregateID string + if req.Id != nil { + aggregateID = *req.Id + } + return &command.AddProject{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: req.OrganizationId, + AggregateID: aggregateID, + }, + Name: req.Name, + ProjectRoleAssertion: req.ProjectRoleAssertion, + ProjectRoleCheck: req.AuthorizationRequired, + HasProjectCheck: req.ProjectAccessRequired, + PrivateLabelingSetting: privateLabelingSettingToDomain(req.PrivateLabelingSetting), + } +} + +func privateLabelingSettingToDomain(setting project_pb.PrivateLabelingSetting) domain.PrivateLabelingSetting { + switch setting { + case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY: + return domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy + case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY: + return domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy + case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED: + return domain.PrivateLabelingSettingUnspecified + default: + return domain.PrivateLabelingSettingUnspecified + } +} + +func (s *Server) UpdateProject(ctx context.Context, req *project_pb.UpdateProjectRequest) (*project_pb.UpdateProjectResponse, error) { + project, err := s.command.ChangeProject(ctx, projectUpdateToCommand(req)) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + changeDate = timestamppb.New(project.EventDate) + } + return &project_pb.UpdateProjectResponse{ + ChangeDate: changeDate, + }, nil +} + +func projectUpdateToCommand(req *project_pb.UpdateProjectRequest) *command.ChangeProject { + var labeling *domain.PrivateLabelingSetting + if req.PrivateLabelingSetting != nil { + labeling = gu.Ptr(privateLabelingSettingToDomain(*req.PrivateLabelingSetting)) + } + return &command.ChangeProject{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.Id, + }, + Name: req.Name, + ProjectRoleAssertion: req.ProjectRoleAssertion, + ProjectRoleCheck: req.ProjectRoleCheck, + HasProjectCheck: req.HasProjectCheck, + PrivateLabelingSetting: labeling, + } +} + +func (s *Server) DeleteProject(ctx context.Context, req *project_pb.DeleteProjectRequest) (*project_pb.DeleteProjectResponse, error) { + userGrantIDs, err := s.userGrantsFromProject(ctx, req.Id) + if err != nil { + return nil, err + } + + deletedAt, err := s.command.DeleteProject(ctx, req.Id, "", userGrantIDs...) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !deletedAt.IsZero() { + deletionDate = timestamppb.New(deletedAt) + } + return &project_pb.DeleteProjectResponse{ + DeletionDate: deletionDate, + }, nil +} + +func (s *Server) userGrantsFromProject(ctx context.Context, projectID string) ([]string, error) { + projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) + if err != nil { + return nil, err + } + userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{projectQuery}, + }, false) + if err != nil { + return nil, err + } + return userGrantsToIDs(userGrants.UserGrants), nil +} + +func (s *Server) DeactivateProject(ctx context.Context, req *project_pb.DeactivateProjectRequest) (*project_pb.DeactivateProjectResponse, error) { + details, err := s.command.DeactivateProject(ctx, req.Id, "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return &project_pb.DeactivateProjectResponse{ + ChangeDate: changeDate, + }, nil +} + +func (s *Server) ActivateProject(ctx context.Context, req *project_pb.ActivateProjectRequest) (*project_pb.ActivateProjectResponse, error) { + details, err := s.command.ReactivateProject(ctx, req.Id, "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return &project_pb.ActivateProjectResponse{ + ChangeDate: changeDate, + }, nil +} + +func userGrantsToIDs(userGrants []*query.UserGrant) []string { + converted := make([]string, len(userGrants)) + for i, grant := range userGrants { + converted[i] = grant.ID + } + return converted +} diff --git a/internal/api/grpc/project/v2beta/project_grant.go b/internal/api/grpc/project/v2beta/project_grant.go new file mode 100644 index 0000000000..6c3b195c66 --- /dev/null +++ b/internal/api/grpc/project/v2beta/project_grant.go @@ -0,0 +1,126 @@ +package project + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func (s *Server) CreateProjectGrant(ctx context.Context, req *project_pb.CreateProjectGrantRequest) (*project_pb.CreateProjectGrantResponse, error) { + add := projectGrantCreateToCommand(req) + project, err := s.command.AddProjectGrant(ctx, add) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + creationDate = timestamppb.New(project.EventDate) + } + return &project_pb.CreateProjectGrantResponse{ + CreationDate: creationDate, + }, nil +} + +func projectGrantCreateToCommand(req *project_pb.CreateProjectGrantRequest) *command.AddProjectGrant { + return &command.AddProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + GrantID: req.GrantedOrganizationId, + GrantedOrgID: req.GrantedOrganizationId, + RoleKeys: req.RoleKeys, + } +} + +func (s *Server) UpdateProjectGrant(ctx context.Context, req *project_pb.UpdateProjectGrantRequest) (*project_pb.UpdateProjectGrantResponse, error) { + project, err := s.command.ChangeProjectGrant(ctx, projectGrantUpdateToCommand(req)) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + changeDate = timestamppb.New(project.EventDate) + } + return &project_pb.UpdateProjectGrantResponse{ + ChangeDate: changeDate, + }, nil +} + +func projectGrantUpdateToCommand(req *project_pb.UpdateProjectGrantRequest) *command.ChangeProjectGrant { + return &command.ChangeProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + GrantedOrgID: req.GrantedOrganizationId, + RoleKeys: req.RoleKeys, + } +} + +func (s *Server) DeactivateProjectGrant(ctx context.Context, req *project_pb.DeactivateProjectGrantRequest) (*project_pb.DeactivateProjectGrantResponse, error) { + details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, "", req.GrantedOrganizationId, "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return &project_pb.DeactivateProjectGrantResponse{ + ChangeDate: changeDate, + }, nil +} + +func (s *Server) ActivateProjectGrant(ctx context.Context, req *project_pb.ActivateProjectGrantRequest) (*project_pb.ActivateProjectGrantResponse, error) { + details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, "", req.GrantedOrganizationId, "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return &project_pb.ActivateProjectGrantResponse{ + ChangeDate: changeDate, + }, nil +} + +func (s *Server) DeleteProjectGrant(ctx context.Context, req *project_pb.DeleteProjectGrantRequest) (*project_pb.DeleteProjectGrantResponse, error) { + userGrantIDs, err := s.userGrantsFromProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId) + if err != nil { + return nil, err + } + details, err := s.command.DeleteProjectGrant(ctx, req.ProjectId, "", req.GrantedOrganizationId, "", userGrantIDs...) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + deletionDate = timestamppb.New(details.EventDate) + } + return &project_pb.DeleteProjectGrantResponse{ + DeletionDate: deletionDate, + }, nil +} + +func (s *Server) userGrantsFromProjectGrant(ctx context.Context, projectID, grantedOrganizationID string) ([]string, error) { + projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) + if err != nil { + return nil, err + } + grantQuery, err := query.NewUserGrantGrantIDSearchQuery(grantedOrganizationID) + if err != nil { + return nil, err + } + userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{projectQuery, grantQuery}, + }, false) + if err != nil { + return nil, err + } + return userGrantsToIDs(userGrants.UserGrants), nil +} diff --git a/internal/api/grpc/project/v2beta/project_role.go b/internal/api/grpc/project/v2beta/project_role.go new file mode 100644 index 0000000000..07fc4e9eac --- /dev/null +++ b/internal/api/grpc/project/v2beta/project_role.go @@ -0,0 +1,132 @@ +package project + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func (s *Server) AddProjectRole(ctx context.Context, req *project_pb.AddProjectRoleRequest) (*project_pb.AddProjectRoleResponse, error) { + role, err := s.command.AddProjectRole(ctx, addProjectRoleRequestToCommand(req)) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !role.EventDate.IsZero() { + creationDate = timestamppb.New(role.EventDate) + } + return &project_pb.AddProjectRoleResponse{ + CreationDate: creationDate, + }, nil +} + +func addProjectRoleRequestToCommand(req *project_pb.AddProjectRoleRequest) *command.AddProjectRole { + group := "" + if req.Group != nil { + group = *req.Group + } + + return &command.AddProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + Key: req.RoleKey, + DisplayName: req.DisplayName, + Group: group, + } +} + +func (s *Server) UpdateProjectRole(ctx context.Context, req *project_pb.UpdateProjectRoleRequest) (*project_pb.UpdateProjectRoleResponse, error) { + role, err := s.command.ChangeProjectRole(ctx, updateProjectRoleRequestToCommand(req)) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !role.EventDate.IsZero() { + changeDate = timestamppb.New(role.EventDate) + } + return &project_pb.UpdateProjectRoleResponse{ + ChangeDate: changeDate, + }, nil +} + +func updateProjectRoleRequestToCommand(req *project_pb.UpdateProjectRoleRequest) *command.ChangeProjectRole { + displayName := "" + if req.DisplayName != nil { + displayName = *req.DisplayName + } + group := "" + if req.Group != nil { + group = *req.Group + } + + return &command.ChangeProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + Key: req.RoleKey, + DisplayName: displayName, + Group: group, + } +} + +func (s *Server) RemoveProjectRole(ctx context.Context, req *project_pb.RemoveProjectRoleRequest) (*project_pb.RemoveProjectRoleResponse, error) { + userGrantIDs, err := s.userGrantsFromProjectAndRole(ctx, req.ProjectId, req.RoleKey) + if err != nil { + return nil, err + } + projectGrantIDs, err := s.projectGrantsFromProjectAndRole(ctx, req.ProjectId, req.RoleKey) + if err != nil { + return nil, err + } + details, err := s.command.RemoveProjectRole(ctx, req.ProjectId, req.RoleKey, "", projectGrantIDs, userGrantIDs...) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + deletionDate = timestamppb.New(details.EventDate) + } + return &project_pb.RemoveProjectRoleResponse{ + RemovalDate: deletionDate, + }, nil +} + +func (s *Server) userGrantsFromProjectAndRole(ctx context.Context, projectID, roleKey string) ([]string, error) { + projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) + if err != nil { + return nil, err + } + rolesQuery, err := query.NewUserGrantRoleQuery(roleKey) + if err != nil { + return nil, err + } + userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{projectQuery, rolesQuery}, + }, false) + if err != nil { + return nil, err + } + return userGrantsToIDs(userGrants.UserGrants), nil +} + +func (s *Server) projectGrantsFromProjectAndRole(ctx context.Context, projectID, roleKey string) ([]string, error) { + projectGrants, err := s.query.SearchProjectGrantsByProjectIDAndRoleKey(ctx, projectID, roleKey) + if err != nil { + return nil, err + } + return projectGrantsToIDs(projectGrants), nil +} + +func projectGrantsToIDs(projectGrants *query.ProjectGrants) []string { + converted := make([]string, len(projectGrants.ProjectGrants)) + for i, grant := range projectGrants.ProjectGrants { + converted[i] = grant.GrantID + } + return converted +} diff --git a/internal/api/grpc/project/v2beta/query.go b/internal/api/grpc/project/v2beta/query.go new file mode 100644 index 0000000000..42b69a480e --- /dev/null +++ b/internal/api/grpc/project/v2beta/query.go @@ -0,0 +1,428 @@ +package project + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func (s *Server) GetProject(ctx context.Context, req *project_pb.GetProjectRequest) (*project_pb.GetProjectResponse, error) { + project, err := s.query.GetProjectByIDWithPermission(ctx, true, req.Id, s.checkPermission) + if err != nil { + return nil, err + } + return &project_pb.GetProjectResponse{ + Project: projectToPb(project), + }, nil +} + +func (s *Server) ListProjects(ctx context.Context, req *project_pb.ListProjectsRequest) (*project_pb.ListProjectsResponse, error) { + queries, err := s.listProjectRequestToModel(req) + if err != nil { + return nil, err + } + resp, err := s.query.SearchGrantedProjects(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return &project_pb.ListProjectsResponse{ + Projects: grantedProjectsToPb(resp.GrantedProjects), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }, nil +} + +func (s *Server) listProjectRequestToModel(req *project_pb.ListProjectsRequest) (*query.ProjectAndGrantedProjectSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := projectFiltersToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.ProjectAndGrantedProjectSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: grantedProjectFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func grantedProjectFieldNameToSortingColumn(field *project_pb.ProjectFieldName) query.Column { + if field == nil { + return query.GrantedProjectColumnCreationDate + } + switch *field { + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_CREATION_DATE: + return query.GrantedProjectColumnCreationDate + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_ID: + return query.GrantedProjectColumnID + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_NAME: + return query.GrantedProjectColumnName + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_CHANGE_DATE: + return query.GrantedProjectColumnChangeDate + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_UNSPECIFIED: + return query.GrantedProjectColumnCreationDate + default: + return query.GrantedProjectColumnCreationDate + } +} + +func projectFiltersToQuery(queries []*project_pb.ProjectSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = projectFilterToModel(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func projectFilterToModel(filter *project_pb.ProjectSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *project_pb.ProjectSearchFilter_ProjectNameFilter: + return projectNameFilterToQuery(q.ProjectNameFilter) + case *project_pb.ProjectSearchFilter_InProjectIdsFilter: + return projectInIDsFilterToQuery(q.InProjectIdsFilter) + case *project_pb.ProjectSearchFilter_ProjectResourceOwnerFilter: + return projectResourceOwnerFilterToQuery(q.ProjectResourceOwnerFilter) + case *project_pb.ProjectSearchFilter_ProjectOrganizationIdFilter: + return projectOrganizationIDFilterToQuery(q.ProjectOrganizationIdFilter) + case *project_pb.ProjectSearchFilter_ProjectGrantResourceOwnerFilter: + return projectGrantResourceOwnerFilterToQuery(q.ProjectGrantResourceOwnerFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func projectNameFilterToQuery(q *project_pb.ProjectNameFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetProjectName()) +} + +func projectInIDsFilterToQuery(q *filter_pb.InIDsFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectIDSearchQuery(q.Ids) +} + +func projectResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectResourceOwnerSearchQuery(q.Id) +} + +func projectOrganizationIDFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectOrganizationIDSearchQuery(q.Id) +} + +func projectGrantResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectGrantResourceOwnerSearchQuery(q.Id) +} + +func grantedProjectsToPb(projects []*query.GrantedProject) []*project_pb.Project { + o := make([]*project_pb.Project, len(projects)) + for i, org := range projects { + o[i] = grantedProjectToPb(org) + } + return o +} + +func projectToPb(project *query.Project) *project_pb.Project { + return &project_pb.Project{ + Id: project.ID, + OrganizationId: project.ResourceOwner, + CreationDate: timestamppb.New(project.CreationDate), + ChangeDate: timestamppb.New(project.ChangeDate), + State: projectStateToPb(project.State), + Name: project.Name, + PrivateLabelingSetting: privateLabelingSettingToPb(project.PrivateLabelingSetting), + ProjectAccessRequired: project.HasProjectCheck, + ProjectRoleAssertion: project.ProjectRoleAssertion, + AuthorizationRequired: project.ProjectRoleCheck, + } +} + +func grantedProjectToPb(project *query.GrantedProject) *project_pb.Project { + var grantedOrganizationID, grantedOrganizationName *string + if project.GrantedOrgID != "" { + grantedOrganizationID = &project.GrantedOrgID + } + if project.OrgName != "" { + grantedOrganizationName = &project.OrgName + } + + return &project_pb.Project{ + Id: project.ProjectID, + OrganizationId: project.ResourceOwner, + CreationDate: timestamppb.New(project.CreationDate), + ChangeDate: timestamppb.New(project.ChangeDate), + State: projectStateToPb(project.ProjectState), + Name: project.ProjectName, + PrivateLabelingSetting: privateLabelingSettingToPb(project.PrivateLabelingSetting), + ProjectAccessRequired: project.HasProjectCheck, + ProjectRoleAssertion: project.ProjectRoleAssertion, + AuthorizationRequired: project.ProjectRoleCheck, + GrantedOrganizationId: grantedOrganizationID, + GrantedOrganizationName: grantedOrganizationName, + GrantedState: grantedProjectStateToPb(project.ProjectGrantState), + } +} + +func projectStateToPb(state domain.ProjectState) project_pb.ProjectState { + switch state { + case domain.ProjectStateActive: + return project_pb.ProjectState_PROJECT_STATE_ACTIVE + case domain.ProjectStateInactive: + return project_pb.ProjectState_PROJECT_STATE_INACTIVE + case domain.ProjectStateUnspecified, domain.ProjectStateRemoved: + return project_pb.ProjectState_PROJECT_STATE_UNSPECIFIED + default: + return project_pb.ProjectState_PROJECT_STATE_UNSPECIFIED + } +} +func grantedProjectStateToPb(state domain.ProjectGrantState) project_pb.GrantedProjectState { + switch state { + case domain.ProjectGrantStateActive: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_ACTIVE + case domain.ProjectGrantStateInactive: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_INACTIVE + case domain.ProjectGrantStateUnspecified, domain.ProjectGrantStateRemoved: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_UNSPECIFIED + default: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_UNSPECIFIED + } +} + +func privateLabelingSettingToPb(setting domain.PrivateLabelingSetting) project_pb.PrivateLabelingSetting { + switch setting { + case domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY + case domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY + case domain.PrivateLabelingSettingUnspecified: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED + default: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED + } +} + +func (s *Server) ListProjectGrants(ctx context.Context, req *project_pb.ListProjectGrantsRequest) (*project_pb.ListProjectGrantsResponse, error) { + queries, err := s.listProjectGrantsRequestToModel(req) + if err != nil { + return nil, err + } + resp, err := s.query.SearchProjectGrants(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return &project_pb.ListProjectGrantsResponse{ + ProjectGrants: projectGrantsToPb(resp.ProjectGrants), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }, nil +} + +func (s *Server) listProjectGrantsRequestToModel(req *project_pb.ListProjectGrantsRequest) (*query.ProjectGrantSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := projectGrantFiltersToModel(req.Filters) + if err != nil { + return nil, err + } + return &query.ProjectGrantSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: projectGrantFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func projectGrantFieldNameToSortingColumn(field *project_pb.ProjectGrantFieldName) query.Column { + if field == nil { + return query.ProjectGrantColumnCreationDate + } + switch *field { + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_PROJECT_ID: + return query.ProjectGrantColumnProjectID + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_CREATION_DATE: + return query.ProjectGrantColumnCreationDate + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_CHANGE_DATE: + return query.ProjectGrantColumnChangeDate + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_UNSPECIFIED: + return query.ProjectGrantColumnCreationDate + default: + return query.ProjectGrantColumnCreationDate + } +} + +func projectGrantFiltersToModel(queries []*project_pb.ProjectGrantSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = projectGrantFilterToModel(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func projectGrantFilterToModel(filter *project_pb.ProjectGrantSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *project_pb.ProjectGrantSearchFilter_ProjectNameFilter: + return projectNameFilterToQuery(q.ProjectNameFilter) + case *project_pb.ProjectGrantSearchFilter_RoleKeyFilter: + return query.NewProjectGrantRoleKeySearchQuery(q.RoleKeyFilter.Key) + case *project_pb.ProjectGrantSearchFilter_InProjectIdsFilter: + return query.NewProjectGrantProjectIDsSearchQuery(q.InProjectIdsFilter.Ids) + case *project_pb.ProjectGrantSearchFilter_ProjectResourceOwnerFilter: + return query.NewProjectGrantResourceOwnerSearchQuery(q.ProjectResourceOwnerFilter.Id) + case *project_pb.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter: + return query.NewProjectGrantGrantedOrgIDSearchQuery(q.ProjectGrantResourceOwnerFilter.Id) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-M099f", "List.Query.Invalid") + } +} + +func projectGrantsToPb(projects []*query.ProjectGrant) []*project_pb.ProjectGrant { + p := make([]*project_pb.ProjectGrant, len(projects)) + for i, project := range projects { + p[i] = projectGrantToPb(project) + } + return p +} + +func projectGrantToPb(project *query.ProjectGrant) *project_pb.ProjectGrant { + return &project_pb.ProjectGrant{ + OrganizationId: project.ResourceOwner, + CreationDate: timestamppb.New(project.CreationDate), + ChangeDate: timestamppb.New(project.ChangeDate), + GrantedOrganizationId: project.GrantedOrgID, + GrantedOrganizationName: project.OrgName, + GrantedRoleKeys: project.GrantedRoleKeys, + ProjectId: project.ProjectID, + ProjectName: project.ProjectName, + State: projectGrantStateToPb(project.State), + } +} + +func projectGrantStateToPb(state domain.ProjectGrantState) project_pb.ProjectGrantState { + switch state { + case domain.ProjectGrantStateActive: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_ACTIVE + case domain.ProjectGrantStateInactive: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_INACTIVE + case domain.ProjectGrantStateUnspecified, domain.ProjectGrantStateRemoved: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_UNSPECIFIED + default: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_UNSPECIFIED + } +} + +func (s *Server) ListProjectRoles(ctx context.Context, req *project_pb.ListProjectRolesRequest) (*project_pb.ListProjectRolesResponse, error) { + queries, err := s.listProjectRolesRequestToModel(req) + if err != nil { + return nil, err + } + err = queries.AppendProjectIDQuery(req.ProjectId) + if err != nil { + return nil, err + } + roles, err := s.query.SearchProjectRoles(ctx, true, queries, s.checkPermission) + if err != nil { + return nil, err + } + return &project_pb.ListProjectRolesResponse{ + ProjectRoles: roleViewsToPb(roles.ProjectRoles), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, roles.SearchResponse), + }, nil +} + +func (s *Server) listProjectRolesRequestToModel(req *project_pb.ListProjectRolesRequest) (*query.ProjectRoleSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := roleQueriesToModel(req.Filters) + if err != nil { + return nil, err + } + return &query.ProjectRoleSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: projectRoleFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func projectRoleFieldNameToSortingColumn(field *project_pb.ProjectRoleFieldName) query.Column { + if field == nil { + return query.ProjectRoleColumnCreationDate + } + switch *field { + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_KEY: + return query.ProjectRoleColumnKey + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_CREATION_DATE: + return query.ProjectRoleColumnCreationDate + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_CHANGE_DATE: + return query.ProjectRoleColumnChangeDate + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_UNSPECIFIED: + return query.ProjectRoleColumnCreationDate + default: + return query.ProjectRoleColumnCreationDate + } +} + +func roleQueriesToModel(queries []*project_pb.ProjectRoleSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = roleQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func roleQueryToModel(apiQuery *project_pb.ProjectRoleSearchFilter) (query.SearchQuery, error) { + switch q := apiQuery.Filter.(type) { + case *project_pb.ProjectRoleSearchFilter_RoleKeyFilter: + return query.NewProjectRoleKeySearchQuery(filter.TextMethodPbToQuery(q.RoleKeyFilter.Method), q.RoleKeyFilter.Key) + case *project_pb.ProjectRoleSearchFilter_DisplayNameFilter: + return query.NewProjectRoleDisplayNameSearchQuery(filter.TextMethodPbToQuery(q.DisplayNameFilter.Method), q.DisplayNameFilter.DisplayName) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-fms0e", "List.Query.Invalid") + } +} + +func roleViewsToPb(roles []*query.ProjectRole) []*project_pb.ProjectRole { + o := make([]*project_pb.ProjectRole, len(roles)) + for i, org := range roles { + o[i] = roleViewToPb(org) + } + return o +} + +func roleViewToPb(role *query.ProjectRole) *project_pb.ProjectRole { + return &project_pb.ProjectRole{ + ProjectId: role.ProjectID, + Key: role.Key, + CreationDate: timestamppb.New(role.CreationDate), + ChangeDate: timestamppb.New(role.ChangeDate), + DisplayName: role.DisplayName, + Group: role.Group, + } +} diff --git a/internal/api/grpc/project/v2beta/server.go b/internal/api/grpc/project/v2beta/server.go new file mode 100644 index 0000000000..fe197f9688 --- /dev/null +++ b/internal/api/grpc/project/v2beta/server.go @@ -0,0 +1,60 @@ +package project + +import ( + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +var _ project.ProjectServiceServer = (*Server)(nil) + +type Server struct { + project.UnimplementedProjectServiceServer + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + + checkPermission domain.PermissionCheck +} + +type Config struct{} + +func CreateServer( + systemDefaults systemdefaults.SystemDefaults, + command *command.Commands, + query *query.Queries, + checkPermission domain.PermissionCheck, +) *Server { + return &Server{ + systemDefaults: systemDefaults, + command: command, + query: query, + checkPermission: checkPermission, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + project.RegisterProjectServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return project.ProjectService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return project.ProjectService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return project.ProjectService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return project.RegisterProjectServiceHandler +} diff --git a/internal/api/grpc/saml/v2/integration/saml_test.go b/internal/api/grpc/saml/v2/integration/saml_test.go index 1f227ab149..1974c5236a 100644 --- a/internal/api/grpc/saml/v2/integration/saml_test.go +++ b/internal/api/grpc/saml/v2/integration/saml_test.go @@ -332,7 +332,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID2, _, _ := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true) orgResp := Instance.CreateOrganization(ctx, "saml-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID2, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID2, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -346,7 +346,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true) orgResp := Instance.CreateOrganization(ctx, "saml-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -368,9 +368,9 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true) orgResp := Instance.CreateOrganization(ctx, "saml-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) }, @@ -502,9 +502,9 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, false) orgResp := Instance.CreateOrganization(ctx, "saml-permissison-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) }, @@ -523,7 +523,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, false) orgResp := Instance.CreateOrganization(ctx, "saml-permissison-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) @@ -563,7 +563,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, false, true) orgResp := Instance.CreateOrganization(ctx, "saml-permissison-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) @@ -640,10 +640,9 @@ func createSAMLSP(t *testing.T, idpMetadata *saml.EntityDescriptor, binding stri } func createSAMLApplication(ctx context.Context, t *testing.T, idpMetadata *saml.EntityDescriptor, binding string, projectRoleCheck, hasProjectCheck bool) (string, string, *samlsp.Middleware) { - project, err := Instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) - require.NoError(t, err) + project := Instance.CreateProject(ctx, t, "", gofakeit.AppName(), projectRoleCheck, hasProjectCheck) rootURL, sp := createSAMLSP(t, idpMetadata, binding) - _, err = Instance.CreateSAMLClient(ctx, project.GetId(), sp) + _, err := Instance.CreateSAMLClient(ctx, project.GetId(), sp) require.NoError(t, err) return project.GetId(), rootURL, sp } 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 b9a060c749..0982a56121 100644 --- a/internal/api/grpc/session/v2/integration_test/session_test.go +++ b/internal/api/grpc/session/v2/integration_test/session_test.go @@ -354,7 +354,7 @@ 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, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), @@ -372,7 +372,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ @@ -396,7 +396,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { // successful intent without known / linked user idpUserID := "id" - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, "") + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, "", time.Now().Add(time.Hour)) // link the user (with info from intent) Instance.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId()) @@ -447,6 +447,80 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { require.Error(t, err) } +func TestServer_CreateSession_reuseIntent(t *testing.T) { + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) + require.NoError(t, err) + updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor) + + // the reuse of the intent token is not allowed, not even on the same session + session2, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.Error(t, err) + _ = session2 +} + +func TestServer_CreateSession_expiredIntent(t *testing.T) { + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Second)) + require.NoError(t, err) + + // wait for the intent to expire + time.Sleep(2 * time.Second) + + _, err = Client.SetSession(LoginCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + 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{ UserId: userID, 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 d0fc1179ef..4c189e0f80 100644 --- a/internal/api/grpc/session/v2beta/integration_test/session_test.go +++ b/internal/api/grpc/session/v2beta/integration_test/session_test.go @@ -354,7 +354,7 @@ 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, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), @@ -372,7 +372,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ @@ -396,7 +396,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { // successful intent without known / linked user idpUserID := "id" - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) // link the user (with info from intent) @@ -448,6 +448,80 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { require.Error(t, err) } +func TestServer_CreateSession_reuseIntent(t *testing.T) { + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(IAMOwnerCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) + require.NoError(t, err) + updateResp, err := Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor) + + // the reuse of the intent token is not allowed, not even on the same session + session2, err := Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.Error(t, err) + _ = session2 +} + +func TestServer_CreateSession_expiredIntent(t *testing.T) { + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(IAMOwnerCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Second)) + require.NoError(t, err) + + // wait for the intent to expire + time.Sleep(2 * time.Second) + + _, err = Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + 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{ UserId: userID, diff --git a/internal/api/grpc/user/query.go b/internal/api/grpc/user/query.go index 41cce01a8c..66edbac90e 100644 --- a/internal/api/grpc/user/query.go +++ b/internal/api/grpc/user/query.go @@ -84,11 +84,11 @@ func EmailQueryToQuery(q *user_pb.EmailQuery) (query.SearchQuery, error) { } func StateQueryToQuery(q *user_pb.StateQuery) (query.SearchQuery, error) { - return query.NewUserStateSearchQuery(int32(q.State)) + return query.NewUserStateSearchQuery(q.State.ToDomain()) } func TypeQueryToQuery(q *user_pb.TypeQuery) (query.SearchQuery, error) { - return query.NewUserTypeSearchQuery(int32(q.Type)) + return query.NewUserTypeSearchQuery(q.Type.ToDomain()) } func LoginNameQueryToQuery(q *user_pb.LoginNameQuery) (query.SearchQuery, error) { diff --git a/internal/api/grpc/user/v2/human.go b/internal/api/grpc/user/v2/human.go new file mode 100644 index 0000000000..d8a0891396 --- /dev/null +++ b/internal/api/grpc/user/v2/human.go @@ -0,0 +1,187 @@ +package user + +import ( + "context" + "io" + + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + legacyobject "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUserRequest_Human, orgId string, userName, userId *string) (*user.CreateUserResponse, error) { + addHumanPb := &user.AddHumanUserRequest{ + Username: userName, + UserId: userId, + Organization: &legacyobject.Organization{ + Org: &legacyobject.Organization_OrgId{OrgId: orgId}, + }, + Profile: humanPb.Profile, + Email: humanPb.Email, + Phone: humanPb.Phone, + IdpLinks: humanPb.IdpLinks, + TotpSecret: humanPb.TotpSecret, + } + switch pwType := humanPb.GetPasswordType().(type) { + case *user.CreateUserRequest_Human_HashedPassword: + addHumanPb.PasswordType = &user.AddHumanUserRequest_HashedPassword{ + HashedPassword: pwType.HashedPassword, + } + case *user.CreateUserRequest_Human_Password: + addHumanPb.PasswordType = &user.AddHumanUserRequest_Password{ + Password: pwType.Password, + } + default: + // optional password is not set + } + newHuman, err := AddUserRequestToAddHuman(addHumanPb) + if err != nil { + return nil, err + } + if err = s.command.AddUserHuman( + ctx, + orgId, + newHuman, + false, + s.userCodeAlg, + ); err != nil { + return nil, err + } + return &user.CreateUserResponse{ + Id: newHuman.ID, + CreationDate: timestamppb.New(newHuman.Details.EventDate), + EmailCode: newHuman.EmailCode, + PhoneCode: newHuman.PhoneCode, + }, nil +} + +func (s *Server) updateUserTypeHuman(ctx context.Context, humanPb *user.UpdateUserRequest_Human, userId string, userName *string) (*user.UpdateUserResponse, error) { + cmd, err := updateHumanUserToCommand(userId, userName, humanPb) + if err != nil { + return nil, err + } + if err = s.command.ChangeUserHuman(ctx, cmd, s.userCodeAlg); err != nil { + return nil, err + } + return &user.UpdateUserResponse{ + ChangeDate: timestamppb.New(cmd.Details.EventDate), + EmailCode: cmd.EmailCode, + PhoneCode: cmd.PhoneCode, + }, nil +} + +func updateHumanUserToCommand(userId string, userName *string, human *user.UpdateUserRequest_Human) (*command.ChangeHuman, error) { + phone := human.GetPhone() + if phone != nil && phone.Phone == "" && phone.GetVerification() != nil { + return nil, zerrors.ThrowInvalidArgument(nil, "USERv2-4f3d6", "Errors.User.Phone.VerifyingRemovalIsNotSupported") + } + email, err := setHumanEmailToEmail(human.Email, userId) + if err != nil { + return nil, err + } + return &command.ChangeHuman{ + ID: userId, + Username: userName, + Profile: SetHumanProfileToProfile(human.Profile), + Email: email, + Phone: setHumanPhoneToPhone(human.Phone, true), + Password: setHumanPasswordToPassword(human.Password), + }, nil +} + +func updateHumanUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) { + email, err := setHumanEmailToEmail(req.Email, req.GetUserId()) + if err != nil { + return nil, err + } + changeHuman := &command.ChangeHuman{ + ID: req.GetUserId(), + Username: req.Username, + Email: email, + Phone: setHumanPhoneToPhone(req.Phone, false), + Password: setHumanPasswordToPassword(req.Password), + } + if profile := req.GetProfile(); profile != nil { + var firstName *string + if profile.GivenName != "" { + firstName = &profile.GivenName + } + var lastName *string + if profile.FamilyName != "" { + lastName = &profile.FamilyName + } + changeHuman.Profile = SetHumanProfileToProfile(&user.UpdateUserRequest_Human_Profile{ + GivenName: firstName, + FamilyName: lastName, + NickName: profile.NickName, + DisplayName: profile.DisplayName, + PreferredLanguage: profile.PreferredLanguage, + Gender: profile.Gender, + }) + } + return changeHuman, nil +} + +func SetHumanProfileToProfile(profile *user.UpdateUserRequest_Human_Profile) *command.Profile { + if profile == nil { + return nil + } + return &command.Profile{ + FirstName: profile.GivenName, + LastName: profile.FamilyName, + NickName: profile.NickName, + DisplayName: profile.DisplayName, + PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make), + Gender: ifNotNilPtr(profile.Gender, genderToDomain), + } +} + +func setHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) { + if email == nil { + return nil, nil + } + var urlTemplate string + if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil { + urlTemplate = *email.GetSendCode().UrlTemplate + if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil { + return nil, err + } + } + return &command.Email{ + Address: domain.EmailAddress(email.Email), + Verified: email.GetIsVerified(), + ReturnCode: email.GetReturnCode() != nil, + URLTemplate: urlTemplate, + }, nil +} + +func setHumanPhoneToPhone(phone *user.SetHumanPhone, withRemove bool) *command.Phone { + if phone == nil { + return nil + } + number := phone.GetPhone() + return &command.Phone{ + Number: domain.PhoneNumber(number), + Verified: phone.GetIsVerified(), + ReturnCode: phone.GetReturnCode() != nil, + Remove: withRemove && number == "", + } +} + +func setHumanPasswordToPassword(password *user.SetPassword) *command.Password { + if password == nil { + return nil + } + return &command.Password{ + PasswordCode: password.GetVerificationCode(), + OldPassword: password.GetCurrentPassword(), + Password: password.GetPassword().GetPassword(), + EncodedPasswordHash: password.GetHashedPassword().GetHash(), + ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(), + } +} diff --git a/internal/api/grpc/user/v2/human_test.go b/internal/api/grpc/user/v2/human_test.go new file mode 100644 index 0000000000..52e5371dcc --- /dev/null +++ b/internal/api/grpc/user/v2/human_test.go @@ -0,0 +1,254 @@ +package user + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func Test_patchHumanUserToCommand(t *testing.T) { + type args struct { + userId string + userName *string + human *user.UpdateUserRequest_Human + } + tests := []struct { + name string + args args + want *command.ChangeHuman + wantErr assert.ErrorAssertionFunc + }{{ + name: "single property", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("givenName"), + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Profile: &command.Profile{ + FirstName: gu.Ptr("givenName"), + }, + }, + wantErr: assert.NoError, + }, { + name: "all properties", + args: args{ + userId: "userId", + userName: gu.Ptr("userName"), + human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("givenName"), + FamilyName: gu.Ptr("familyName"), + NickName: gu.Ptr("nickName"), + DisplayName: gu.Ptr("displayName"), + PreferredLanguage: gu.Ptr("en-US"), + Gender: gu.Ptr(user.Gender_GENDER_FEMALE), + }, + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + Phone: &user.SetHumanPhone{ + Phone: "+123456789", + Verification: &user.SetHumanPhone_IsVerified{ + IsVerified: true, + }, + }, + Password: &user.SetPassword{ + Verification: &user.SetPassword_CurrentPassword{ + CurrentPassword: "currentPassword", + }, + PasswordType: &user.SetPassword_Password{ + Password: &user.Password{ + Password: "newPassword", + ChangeRequired: true, + }, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Username: gu.Ptr("userName"), + Profile: &command.Profile{ + FirstName: gu.Ptr("givenName"), + LastName: gu.Ptr("familyName"), + NickName: gu.Ptr("nickName"), + DisplayName: gu.Ptr("displayName"), + PreferredLanguage: &language.AmericanEnglish, + Gender: gu.Ptr(domain.GenderFemale), + }, + Email: &command.Email{ + Address: "email@example.com", + Verified: true, + }, + Phone: &command.Phone{ + Number: "+123456789", + Verified: true, + }, + Password: &command.Password{ + OldPassword: "currentPassword", + Password: "newPassword", + ChangeRequired: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "set email and request code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Email: &command.Email{ + Address: "email@example.com", + ReturnCode: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "set email and send code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Email: &command.Email{ + Address: "email@example.com", + }, + }, + wantErr: assert.NoError, + }, { + name: "set email and send code with template", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("Code: {{.Code}}"), + }, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Email: &command.Email{ + Address: "email@example.com", + URLTemplate: "Code: {{.Code}}", + }, + }, + wantErr: assert.NoError, + }, { + name: "set phone and request code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Phone: "+123456789", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Phone: &command.Phone{ + Number: "+123456789", + ReturnCode: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "set phone and send code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Phone: "+123456789", + Verification: &user.SetHumanPhone_SendCode{ + SendCode: &user.SendPhoneVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Phone: &command.Phone{ + Number: "+123456789", + }, + }, + wantErr: assert.NoError, + }, { + name: "remove phone, ok", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{}, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Phone: &command.Phone{ + Remove: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "remove phone with verification, error", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Verification: &user.SetHumanPhone_ReturnCode{}, + }, + }, + }, + wantErr: assert.Error, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := updateHumanUserToCommand(tt.args.userId, tt.args.userName, tt.args.human) + if !tt.wantErr(t, err, fmt.Sprintf("patchHumanUserToCommand(%v, %v, %v)", tt.args.userId, tt.args.userName, tt.args.human)) { + return + } + if diff := cmp.Diff(tt.want, got, cmpopts.EquateComparable(language.Tag{})); diff != "" { + t.Errorf("patchHumanUserToCommand() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/api/grpc/user/v2/integration_test/email_test.go b/internal/api/grpc/user/v2/integration_test/email_test.go index ad63c2ce5e..ad68ef5c5a 100644 --- a/internal/api/grpc/user/v2/integration_test/email_test.go +++ b/internal/api/grpc/user/v2/integration_test/email_test.go @@ -10,13 +10,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" - + "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func TestServer_SetEmail(t *testing.T) { +func TestServer_Deprecated_SetEmail(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { diff --git a/internal/api/grpc/user/v2/integration_test/key_test.go b/internal/api/grpc/user/v2/integration_test/key_test.go new file mode 100644 index 0000000000..e85903b2cb --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/key_test.go @@ -0,0 +1,659 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AddKey(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.AddKeyRequest + prepare func(request *user.AddKeyRequest) error + } + tests := []struct { + name string + args args + wantErr bool + wantEmtpyKey bool + }{ + { + name: "add key, user not existing", + args: args{ + &user.AddKeyRequest{ + UserId: "notexisting", + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "generate key pair, ok", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + return nil + }, + }, + }, + { + name: "add valid public key, ok", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + // This is the public key of the tester system user. This must be valid. + PublicKey: []byte(` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzi+FFSJL7f5yw4KTwzgM +P34ePGycm/M+kT0M7V4Cgx5V3EaDIvTQKTLfBaEB45zb9LtjIXzDw0rXRoS2hO6t +h+CYQCz3KCvh09C0IzxZiB2IS3H/aT+5Bx9EFY+vnAkZjccbyG5YNRvmtOlnvIeI +H7qZ0tEwkPfF5GEZNPJPtmy3UGV7iofdVQS1xRj73+aMw5rvH4D8IdyiAC3VekIb +pt0Vj0SUX3DwKtog337BzTiPk3aXRF0sbFhQoqdJRI8NqgZjCwjq9yfI5tyxYswn ++JGzHGdHvW3idODlmwEt5K2pasiRIWK2OGfq+w0EcltQHabuqEPgZlmhCkRdNfix +BwIDAQAB +-----END PUBLIC KEY----- +`), + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + return nil + }, + }, + wantEmtpyKey: true, + }, + { + name: "add invalid public key, error", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + PublicKey: []byte(` +-----BEGIN PUBLIC KEY----- +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 +-----END PUBLIC KEY----- +`), + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + return nil + }, + }, + wantErr: true, + }, + { + name: "add key human, error", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { + resp := Instance.CreateUserTypeHuman(IamCTX) + request.UserId = resp.Id + return nil + }, + }, + wantErr: true, + }, + { + name: "add another key, ok", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + _, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.AddKey(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.KeyId, "key id is empty") + if tt.wantEmtpyKey { + assert.Empty(t, got.KeyContent, "key content is not empty") + } else { + assert.NotEmpty(t, got.KeyContent, "key content is empty") + } + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_AddKey_Permission(t *testing.T) { + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddKey-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + request := &user.AddKeyRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + } + type args struct { + ctx context.Context + req *user.AddKeyRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request}, + }, + { + name: "instance, ok", + args: args{IamCTX, request}, + }, + { + name: "org, error", + args: args{OrgCTX, request}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, err) + got, err := Client.AddKey(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.KeyId, "key id is empty") + assert.NotEmpty(t, got.KeyContent, "key content is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveKey(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.RemoveKeyRequest + prepare func(request *user.RemoveKeyRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "remove key, user not existing", + args: args{ + &user.RemoveKeyRequest{ + UserId: "notexisting", + }, + func(request *user.RemoveKeyRequest) error { + key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.KeyId = key.GetKeyId() + return err + }, + }, + wantErr: true, + }, + { + name: "remove key, not existing", + args: args{ + &user.RemoveKeyRequest{ + KeyId: "notexisting", + }, + func(request *user.RemoveKeyRequest) error { + request.UserId = userId + return nil + }, + }, + wantErr: true, + }, + { + name: "remove key, ok", + args: args{ + &user.RemoveKeyRequest{}, + func(request *user.RemoveKeyRequest) error { + key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.KeyId = key.GetKeyId() + request.UserId = userId + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.RemoveKey(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + deletionDate := got.DeletionDate.AsTime() + assert.Greater(t, deletionDate, now, "creation date is before the test started") + assert.Less(t, deletionDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveKey_Permission(t *testing.T) { + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemoveKey-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + request := &user.RemoveKeyRequest{ + UserId: otherOrgUser.GetId(), + } + prepare := func(request *user.RemoveKeyRequest) error { + key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + }) + request.KeyId = key.GetKeyId() + return err + } + require.NoError(t, err) + type args struct { + ctx context.Context + req *user.RemoveKeyRequest + prepare func(request *user.RemoveKeyRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request, prepare}, + }, + { + name: "instance, ok", + args: args{IamCTX, request, prepare}, + }, + { + name: "org, error", + args: args{OrgCTX, request, prepare}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request, prepare}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, tt.args.prepare(tt.args.req)) + got, err := Client.RemoveKey(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "client key is empty") + creationDate := got.DeletionDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_ListKeys(t *testing.T) { + type args struct { + ctx context.Context + req *user.ListKeysRequest + } + type testCase struct { + name string + args args + want *user.ListKeysResponse + } + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(SystemCTX, fmt.Sprintf("ListKeys-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(SystemCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + otherOrgUserId := otherOrgUser.GetId() + otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId() + onlySinceTestStartFilter := &user.KeysSearchFilter{Filter: &user.KeysSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{ + Timestamp: timestamppb.Now(), + Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS, + }}} + myOrgId := Instance.DefaultOrg.GetId() + myUserId := Instance.Users.Get(integration.UserTypeNoPermission).ID + expiresInADay := time.Now().Truncate(time.Hour).Add(time.Hour * 24) + myDataPoint := setupKeyDataPoint(t, myUserId, myOrgId, expiresInADay) + otherUserDataPoint := setupKeyDataPoint(t, otherUserId, myOrgId, expiresInADay) + otherOrgDataPointExpiringSoon := setupKeyDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, time.Now().Truncate(time.Hour).Add(time.Hour)) + otherOrgDataPointExpiringLate := setupKeyDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, expiresInADay.Add(time.Hour*24*30)) + sortingColumnExpirationDate := user.KeyFieldName_KEY_FIELD_NAME_KEY_EXPIRATION_DATE + awaitKeys(t, onlySinceTestStartFilter, + otherOrgDataPointExpiringSoon.GetId(), + otherOrgDataPointExpiringLate.GetId(), + otherUserDataPoint.GetId(), + myDataPoint.GetId(), + ) + tests := []testCase{ + { + name: "list all, instance", + args: args{ + IamCTX, + &user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}}, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, org", + args: args{ + OrgCTX, + &user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}}, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, user", + args: args{ + UserCTX, + &user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}}, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list by id", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.KeysSearchFilter_KeyIdFilter{ + KeyIdFilter: &filter.IDFilter{Id: otherOrgDataPointExpiringSoon.Id}, + }, + }, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all from other org", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.KeysSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}, + }, + }, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "sort by next expiration dates", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + SortingColumn: &sortingColumnExpirationDate, + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + {Filter: &user.KeysSearchFilter_OrganizationIdFilter{OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}}}, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "get page", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Pagination: &filter.PaginationRequest{ + Offset: 2, + Limit: 2, + Asc: true, + }, + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 2, + }, + }, + }, + { + name: "empty list", + args: args{ + UserCTX, + &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{ + { + Filter: &user.KeysSearchFilter_KeyIdFilter{ + KeyIdFilter: &filter.IDFilter{Id: otherUserDataPoint.Id}, + }, + }, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{}, + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + } + t.Run("with permission flag v2", func(t *testing.T) { + setPermissionCheckV2Flag(t, true) + defer setPermissionCheckV2Flag(t, false) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListKeys(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListKeys() mismatch (-want +got):\n%s", diff) + } + }) + } + }) + t.Run("without permission flag v2", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListKeys(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + // ignore the total result, as this is a known bug with the in-memory permission checks. + // The command can't know how many keys exist in the system if the SQL statement has a limit. + // This is fixed, once the in-memory permission checks are removed with https://github.com/zitadel/zitadel/issues/9188 + tt.want.Pagination.TotalResult = got.Pagination.TotalResult + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListKeys() mismatch (-want +got):\n%s", diff) + } + }) + } + }) +} + +func setupKeyDataPoint(t *testing.T, userId, orgId string, expirationDate time.Time) *user.Key { + expirationDatePb := timestamppb.New(expirationDate) + newKey, err := Client.AddKey(SystemCTX, &user.AddKeyRequest{ + UserId: userId, + ExpirationDate: expirationDatePb, + PublicKey: nil, + }) + require.NoError(t, err) + return &user.Key{ + CreationDate: newKey.CreationDate, + ChangeDate: newKey.CreationDate, + Id: newKey.GetKeyId(), + UserId: userId, + OrganizationId: orgId, + ExpirationDate: expirationDatePb, + } +} + +func awaitKeys(t *testing.T, sinceTestStartFilter *user.KeysSearchFilter, keyIds ...string) { + sortingColumn := user.KeyFieldName_KEY_FIELD_NAME_ID + slices.Sort(keyIds) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + result, err := Client.ListKeys(SystemCTX, &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{sinceTestStartFilter}, + SortingColumn: &sortingColumn, + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + }) + require.NoError(t, err) + if !assert.Len(collect, result.Result, len(keyIds)) { + return + } + for i := range keyIds { + keyId := keyIds[i] + require.Equal(collect, keyId, result.Result[i].GetId()) + } + }, 5*time.Second, time.Second, "key not created in time") +} diff --git a/internal/api/grpc/user/v2/integration_test/password_test.go b/internal/api/grpc/user/v2/integration_test/password_test.go index 0cd0da7454..258cdaf78d 100644 --- a/internal/api/grpc/user/v2/integration_test/password_test.go +++ b/internal/api/grpc/user/v2/integration_test/password_test.go @@ -104,7 +104,7 @@ func TestServer_RequestPasswordReset(t *testing.T) { } } -func TestServer_SetPassword(t *testing.T) { +func TestServer_Deprecated_SetPassword(t *testing.T) { type args struct { ctx context.Context req *user.SetPasswordRequest diff --git a/internal/api/grpc/user/v2/integration_test/pat_test.go b/internal/api/grpc/user/v2/integration_test/pat_test.go new file mode 100644 index 0000000000..ce974e0407 --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/pat_test.go @@ -0,0 +1,615 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AddPersonalAccessToken(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.AddPersonalAccessTokenRequest + prepare func(request *user.AddPersonalAccessTokenRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "add pat, user not existing", + args: args{ + &user.AddPersonalAccessTokenRequest{ + UserId: "notexisting", + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "add pat, ok", + args: args{ + &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { + request.UserId = userId + return nil + }, + }, + }, + { + name: "add pat human, not ok", + args: args{ + &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { + resp := Instance.CreateUserTypeHuman(IamCTX) + request.UserId = resp.Id + return nil + }, + }, + wantErr: true, + }, + { + name: "add another pat, ok", + args: args{ + &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { + request.UserId = userId + _, err := Client.AddPersonalAccessToken(IamCTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.AddPersonalAccessToken(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.TokenId, "id is empty") + assert.NotEmpty(t, got.Token, "token is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_AddPersonalAccessToken_Permission(t *testing.T) { + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddPersonalAccessToken-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + request := &user.AddPersonalAccessTokenRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + } + type args struct { + ctx context.Context + req *user.AddPersonalAccessTokenRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request}, + }, + { + name: "instance, ok", + args: args{IamCTX, request}, + }, + { + name: "org, error", + args: args{OrgCTX, request}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, err) + got, err := Client.AddPersonalAccessToken(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.TokenId, "id is empty") + assert.NotEmpty(t, got.Token, "token is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemovePersonalAccessToken(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.RemovePersonalAccessTokenRequest + prepare func(request *user.RemovePersonalAccessTokenRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "remove pat, user not existing", + args: args{ + &user.RemovePersonalAccessTokenRequest{ + UserId: "notexisting", + }, + func(request *user.RemovePersonalAccessTokenRequest) error { + pat, err := Client.AddPersonalAccessToken(CTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.TokenId = pat.GetTokenId() + return err + }, + }, + wantErr: true, + }, + { + name: "remove pat, not existing", + args: args{ + &user.RemovePersonalAccessTokenRequest{ + TokenId: "notexisting", + }, + func(request *user.RemovePersonalAccessTokenRequest) error { + request.UserId = userId + return nil + }, + }, + wantErr: true, + }, + { + name: "remove pat, ok", + args: args{ + &user.RemovePersonalAccessTokenRequest{}, + func(request *user.RemovePersonalAccessTokenRequest) error { + pat, err := Client.AddPersonalAccessToken(CTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.TokenId = pat.GetTokenId() + request.UserId = userId + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.RemovePersonalAccessToken(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + deletionDate := got.DeletionDate.AsTime() + assert.Greater(t, deletionDate, now, "creation date is before the test started") + assert.Less(t, deletionDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemovePersonalAccessToken_Permission(t *testing.T) { + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemovePersonalAccessToken-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + request := &user.RemovePersonalAccessTokenRequest{ + UserId: otherOrgUser.GetId(), + } + prepare := func(request *user.RemovePersonalAccessTokenRequest) error { + pat, err := Client.AddPersonalAccessToken(IamCTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + }) + request.TokenId = pat.GetTokenId() + return err + } + require.NoError(t, err) + type args struct { + ctx context.Context + req *user.RemovePersonalAccessTokenRequest + prepare func(request *user.RemovePersonalAccessTokenRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request, prepare}, + }, + { + name: "instance, ok", + args: args{IamCTX, request, prepare}, + }, + { + name: "org, error", + args: args{CTX, request, prepare}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request, prepare}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, tt.args.prepare(tt.args.req)) + got, err := Client.RemovePersonalAccessToken(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "client pat is empty") + creationDate := got.DeletionDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_ListPersonalAccessTokens(t *testing.T) { + type args struct { + ctx context.Context + req *user.ListPersonalAccessTokensRequest + } + type testCase struct { + name string + args args + want *user.ListPersonalAccessTokensResponse + } + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(SystemCTX, fmt.Sprintf("ListPersonalAccessTokens-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(SystemCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + otherOrgUserId := otherOrgUser.GetId() + otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId() + onlySinceTestStartFilter := &user.PersonalAccessTokensSearchFilter{Filter: &user.PersonalAccessTokensSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{ + Timestamp: timestamppb.Now(), + Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS, + }}} + myOrgId := Instance.DefaultOrg.GetId() + myUserId := Instance.Users.Get(integration.UserTypeNoPermission).ID + expiresInADay := time.Now().Truncate(time.Hour).Add(time.Hour * 24) + myDataPoint := setupPATDataPoint(t, myUserId, myOrgId, expiresInADay) + otherUserDataPoint := setupPATDataPoint(t, otherUserId, myOrgId, expiresInADay) + otherOrgDataPointExpiringSoon := setupPATDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, time.Now().Truncate(time.Hour).Add(time.Hour)) + otherOrgDataPointExpiringLate := setupPATDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, expiresInADay.Add(time.Hour*24*30)) + sortingColumnExpirationDate := user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE + awaitPersonalAccessTokens(t, + onlySinceTestStartFilter, + otherOrgDataPointExpiringSoon.GetId(), + otherOrgDataPointExpiringLate.GetId(), + otherUserDataPoint.GetId(), + myDataPoint.GetId(), + ) + tests := []testCase{ + { + name: "list all, instance", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, org", + args: args{ + OrgCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, user", + args: args{ + UserCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list by id", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.PersonalAccessTokensSearchFilter_TokenIdFilter{ + TokenIdFilter: &filter.IDFilter{Id: otherOrgDataPointExpiringSoon.Id}, + }, + }, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all from other org", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.PersonalAccessTokensSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}, + }, + }}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "sort by next expiration dates", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + SortingColumn: &sortingColumnExpirationDate, + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + {Filter: &user.PersonalAccessTokensSearchFilter_OrganizationIdFilter{OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}}}, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "get page", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Pagination: &filter.PaginationRequest{ + Offset: 2, + Limit: 2, + Asc: true, + }, + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 2, + }, + }, + }, + { + name: "empty list", + args: args{ + UserCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + { + Filter: &user.PersonalAccessTokensSearchFilter_TokenIdFilter{ + TokenIdFilter: &filter.IDFilter{Id: otherUserDataPoint.Id}, + }, + }, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{}, + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + } + t.Run("with permission flag v2", func(t *testing.T) { + setPermissionCheckV2Flag(t, true) + defer setPermissionCheckV2Flag(t, false) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListPersonalAccessTokens(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListPersonalAccessTokens() mismatch (-want +got):\n%s", diff) + } + }) + } + }) + t.Run("without permission flag v2", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListPersonalAccessTokens(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + // ignore the total result, as this is a known bug with the in-memory permission checks. + // The command can't know how many keys exist in the system if the SQL statement has a limit. + // This is fixed, once the in-memory permission checks are removed with https://github.com/zitadel/zitadel/issues/9188 + tt.want.Pagination.TotalResult = got.Pagination.TotalResult + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListPersonalAccessTokens() mismatch (-want +got):\n%s", diff) + } + }) + } + }) +} + +func setupPATDataPoint(t *testing.T, userId, orgId string, expirationDate time.Time) *user.PersonalAccessToken { + expirationDatePb := timestamppb.New(expirationDate) + newPersonalAccessToken, err := Client.AddPersonalAccessToken(SystemCTX, &user.AddPersonalAccessTokenRequest{ + UserId: userId, + ExpirationDate: expirationDatePb, + }) + require.NoError(t, err) + return &user.PersonalAccessToken{ + CreationDate: newPersonalAccessToken.CreationDate, + ChangeDate: newPersonalAccessToken.CreationDate, + Id: newPersonalAccessToken.GetTokenId(), + UserId: userId, + OrganizationId: orgId, + ExpirationDate: expirationDatePb, + } +} + +func awaitPersonalAccessTokens(t *testing.T, sinceTestStartFilter *user.PersonalAccessTokensSearchFilter, patIds ...string) { + sortingColumn := user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID + slices.Sort(patIds) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + result, err := Client.ListPersonalAccessTokens(SystemCTX, &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{sinceTestStartFilter}, + SortingColumn: &sortingColumn, + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + }) + require.NoError(t, err) + if !assert.Len(collect, result.Result, len(patIds)) { + return + } + for i := range patIds { + patId := patIds[i] + require.Equal(collect, patId, result.Result[i].GetId()) + } + }, 5*time.Second, time.Second, "pat not created in time") +} diff --git a/internal/api/grpc/user/v2/integration_test/phone_test.go b/internal/api/grpc/user/v2/integration_test/phone_test.go index 49050c5fe6..b87f9a9f28 100644 --- a/internal/api/grpc/user/v2/integration_test/phone_test.go +++ b/internal/api/grpc/user/v2/integration_test/phone_test.go @@ -17,7 +17,7 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func TestServer_SetPhone(t *testing.T) { +func TestServer_Deprecated_SetPhone(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -249,7 +249,7 @@ func TestServer_VerifyPhone(t *testing.T) { } } -func TestServer_RemovePhone(t *testing.T) { +func TestServer_Deprecated_RemovePhone(t *testing.T) { userResp := Instance.CreateHumanUser(CTX) failResp := Instance.CreateHumanUserNoPhone(CTX) otherUser := Instance.CreateHumanUser(CTX).GetUserId() diff --git a/internal/api/grpc/user/v2/integration_test/secret_test.go b/internal/api/grpc/user/v2/integration_test/secret_test.go new file mode 100644 index 0000000000..8ff537b1fd --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/secret_test.go @@ -0,0 +1,347 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AddSecret(t *testing.T) { + type args struct { + ctx context.Context + req *user.AddSecretRequest + prepare func(request *user.AddSecretRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "add secret, user not existing", + args: args{ + CTX, + &user.AddSecretRequest{ + UserId: "notexisting", + }, + func(request *user.AddSecretRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "add secret, ok", + args: args{ + CTX, + &user.AddSecretRequest{}, + func(request *user.AddSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + return nil + }, + }, + }, + { + name: "add secret human, not ok", + args: args{ + CTX, + &user.AddSecretRequest{}, + func(request *user.AddSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + return nil + }, + }, + }, + { + name: "overwrite secret, ok", + args: args{ + CTX, + &user.AddSecretRequest{}, + func(request *user.AddSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + _, err := Client.AddSecret(CTX, &user.AddSecretRequest{ + UserId: resp.GetId(), + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.AddSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ClientSecret, "client secret is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_AddSecret_Permission(t *testing.T) { + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddSecret-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.AddSecretRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{ + SystemCTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + }, + { + name: "instance, ok", + args: args{ + IamCTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + }, + { + name: "org, error", + args: args{ + CTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + wantErr: true, + }, + { + name: "user, error", + args: args{ + UserCTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, err) + got, err := Client.AddSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ClientSecret, "client secret is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveSecret(t *testing.T) { + type args struct { + ctx context.Context + req *user.RemoveSecretRequest + prepare func(request *user.RemoveSecretRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "remove secret, user not existing", + args: args{ + CTX, + &user.RemoveSecretRequest{ + UserId: "notexisting", + }, + func(request *user.RemoveSecretRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "remove secret, not existing", + args: args{ + CTX, + &user.RemoveSecretRequest{}, + func(request *user.RemoveSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + return nil + }, + }, + wantErr: true, + }, + { + name: "remove secret, ok", + args: args{ + CTX, + &user.RemoveSecretRequest{}, + func(request *user.RemoveSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + _, err := Instance.Client.UserV2.AddSecret(CTX, &user.AddSecretRequest{ + UserId: resp.GetId(), + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.RemoveSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + deletionDate := got.DeletionDate.AsTime() + assert.Greater(t, deletionDate, now, "creation date is before the test started") + assert.Less(t, deletionDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveSecret_Permission(t *testing.T) { + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemoveSecret-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.RemoveSecretRequest + prepare func(request *user.RemoveSecretRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{ + SystemCTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + }, + { + name: "instance, ok", + args: args{ + IamCTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + }, + { + name: "org, error", + args: args{ + CTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + wantErr: true, + }, + { + name: "user, error", + args: args{ + UserCTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, tt.args.prepare(tt.args.req)) + got, err := Client.RemoveSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "client secret is empty") + creationDate := got.DeletionDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} 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 bf396fd25d..4eee44ab44 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -57,7 +57,7 @@ func TestMain(m *testing.M) { }()) } -func TestServer_AddHumanUser(t *testing.T) { +func TestServer_Deprecated_AddHumanUser(t *testing.T) { idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) type args struct { ctx context.Context @@ -652,6 +652,7 @@ func TestServer_AddHumanUser(t *testing.T) { t.Run(tt.name, func(t *testing.T) { userID := fmt.Sprint(time.Now().UnixNano() + int64(i)) tt.args.req.UserId = &userID + // In order to prevent unique constraint errors, we set the email to a unique value if email := tt.args.req.GetEmail(); email != nil { email.Email = fmt.Sprintf("%s@me.now", userID) } @@ -666,7 +667,6 @@ func TestServer_AddHumanUser(t *testing.T) { return } require.NoError(t, err) - assert.Equal(t, tt.want.GetUserId(), got.GetUserId()) if tt.want.GetEmailCode() != "" { assert.NotEmpty(t, got.GetEmailCode()) @@ -683,7 +683,7 @@ func TestServer_AddHumanUser(t *testing.T) { } } -func TestServer_AddHumanUser_Permission(t *testing.T) { +func TestServer_Deprecated_AddHumanUser_Permission(t *testing.T) { newOrgOwnerEmail := gofakeit.Email() newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) type args struct { @@ -876,7 +876,7 @@ func TestServer_AddHumanUser_Permission(t *testing.T) { } } -func TestServer_UpdateHumanUser(t *testing.T) { +func TestServer_Deprecated_UpdateHumanUser(t *testing.T) { type args struct { ctx context.Context req *user.UpdateHumanUserRequest @@ -1237,7 +1237,7 @@ func TestServer_UpdateHumanUser(t *testing.T) { } } -func TestServer_UpdateHumanUser_Permission(t *testing.T) { +func TestServer_Deprecated_UpdateHumanUser_Permission(t *testing.T) { newOrgOwnerEmail := gofakeit.Email() newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("UpdateHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) newUserID := newOrg.CreatedAdmins[0].GetUserId() @@ -1753,12 +1753,11 @@ func TestServer_ReactivateUser(t *testing.T) { } func TestServer_DeleteUser(t *testing.T) { - projectResp, err := Instance.CreateProject(CTX) - require.NoError(t, err) + projectResp := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) + type args struct { - ctx context.Context req *user.DeleteUserRequest - prepare func(request *user.DeleteUserRequest) error + prepare func(*testing.T, *user.DeleteUserRequest) context.Context } tests := []struct { name string @@ -1769,23 +1768,21 @@ func TestServer_DeleteUser(t *testing.T) { { name: "remove, not existing", args: args{ - CTX, &user.DeleteUserRequest{ UserId: "notexisting", }, - func(request *user.DeleteUserRequest) error { return nil }, + func(*testing.T, *user.DeleteUserRequest) context.Context { return CTX }, }, wantErr: true, }, { name: "remove human, ok", args: args{ - ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(_ *testing.T, request *user.DeleteUserRequest) context.Context { resp := Instance.CreateHumanUser(CTX) request.UserId = resp.GetUserId() - return err + return CTX }, }, want: &user.DeleteUserResponse{ @@ -1798,12 +1795,11 @@ func TestServer_DeleteUser(t *testing.T) { { name: "remove machine, ok", args: args{ - ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(_ *testing.T, request *user.DeleteUserRequest) context.Context { resp := Instance.CreateMachineUser(CTX) request.UserId = resp.GetUserId() - return err + return CTX }, }, want: &user.DeleteUserResponse{ @@ -1816,15 +1812,48 @@ func TestServer_DeleteUser(t *testing.T) { { name: "remove dependencies, ok", args: args{ - ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(_ *testing.T, request *user.DeleteUserRequest) context.Context { resp := Instance.CreateHumanUser(CTX) request.UserId = resp.GetUserId() Instance.CreateProjectUserGrant(t, CTX, projectResp.GetId(), request.UserId) Instance.CreateProjectMembership(t, CTX, projectResp.GetId(), request.UserId) Instance.CreateOrgMembership(t, CTX, request.UserId) - return err + return CTX + }, + }, + want: &user.DeleteUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Instance.DefaultOrg.Id, + }, + }, + }, + { + name: "remove self, ok", + args: args{ + req: &user.DeleteUserRequest{}, + prepare: func(t *testing.T, request *user.DeleteUserRequest) context.Context { + removeUser, err := Client.CreateUser(CTX, &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "givenName", + FamilyName: "familyName", + }, + Email: &user.SetHumanEmail{ + Email: gofakeit.Email(), + Verification: &user.SetHumanEmail_IsVerified{IsVerified: true}, + }, + }, + }, + }) + require.NoError(t, err) + request.UserId = removeUser.Id + Instance.RegisterUserPasskey(CTX, removeUser.Id) + _, token, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, removeUser.Id) + return integration.WithAuthorizationToken(UserCTX, token) }, }, want: &user.DeleteUserResponse{ @@ -1837,10 +1866,8 @@ func TestServer_DeleteUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.args.prepare(tt.args.req) - require.NoError(t, err) - - got, err := Client.DeleteUser(tt.args.ctx, tt.args.req) + ctx := tt.args.prepare(t, tt.args.req) + got, err := Client.DeleteUser(ctx, tt.args.req) if tt.wantErr { require.Error(t, err) return @@ -1859,6 +1886,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { samlIdpID := Instance.AddSAMLProvider(IamCTX) samlRedirectIdpID := Instance.AddSAMLRedirectProvider(IamCTX, "") samlPostIdpID := Instance.AddSAMLPostProvider(IamCTX) + jwtIdPID := Instance.AddJWTProvider(IamCTX) type args struct { ctx context.Context req *user.StartIdentityProviderIntentRequest @@ -2081,6 +2109,30 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "next step jwt idp", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: jwtIdPID, + Content: &user.StartIdentityProviderIntentRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: "https://example.com/success", + FailureUrl: "https://example.com/failure", + }, + }, + }, + }, + want: want{ + details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Instance.ID(), + }, + url: "https://example.com/jwt", + parametersExisting: []string{"authRequestID", "userAgentID"}, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2118,25 +2170,44 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, gofakeit.AppName()).GetId() samlIdpID := Instance.AddSAMLPostProvider(IamCTX) ldapIdpID := Instance.AddLDAPProvider(IamCTX) + jwtIdPID := Instance.AddJWTProvider(IamCTX) authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) require.NoError(t, err) intentID := authURL.Query().Get("state") + expiry := time.Now().Add(1 * time.Hour) + expiryFormatted := expiry.Round(time.Millisecond).UTC().Format("2006-01-02T15:04:05.999Z07:00") - successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "") + intentUser := Instance.CreateHumanUser(IamCTX) + _, err = Instance.CreateUserIDPlink(IamCTX, intentUser.GetUserId(), "idpUserID", oauthIdpID, "username") require.NoError(t, err) - successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user") + + successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry) require.NoError(t, err) - oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "") + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry) require.NoError(t, err) - oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user") + successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second)) + require.NoError(t, err) + // make sure the intent is expired + time.Sleep(2 * time.Second) + successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry) + require.NoError(t, err) + // make sure the intent is consumed + Instance.CreateIntentSession(t, IamCTX, intentUser.GetUserId(), successfulConsumedID, consumedToken) + oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "", expiry) + require.NoError(t, err) + oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user", expiry) 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", "") + samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry) require.NoError(t, err) - samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user") + samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry) + require.NoError(t, err) + jwtSuccessfulID, jwtToken, jwtChangeDate, jwtSequence, err := sink.SuccessfulJWTIntent(Instance.ID(), jwtIdPID, "id", "", expiry) + require.NoError(t, err) + jwtSuccessfulWithUserID, jwtWithUserToken, jwtWithUserChangeDate, jwtWithUserSequence, err := sink.SuccessfulJWTIntent(Instance.ID(), jwtIdPID, "id", "user", expiry) require.NoError(t, err) type args struct { ctx context.Context @@ -2260,6 +2331,28 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful expired intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulExpiredID, + IdpIntentToken: expiredToken, + }, + }, + wantErr: true, + }, + { + name: "retrieve successful consumed intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulConsumedID, + IdpIntentToken: consumedToken, + }, + }, + wantErr: true, + }, { name: "retrieve successful oidc intent", args: args{ @@ -2469,7 +2562,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, IdpId: samlIdpID, @@ -2518,7 +2611,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, IdpId: samlIdpID, @@ -2539,6 +2632,88 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful jwt intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: jwtSuccessfulID, + IdpIntentToken: jwtToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(jwtChangeDate), + ResourceOwner: Instance.ID(), + Sequence: jwtSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: jwtIdPID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + }) + require.NoError(t, err) + return s + }(), + }, + AddHumanUser: &user.AddHumanUserRequest{ + Profile: &user.SetHumanProfile{ + PreferredLanguage: gu.Ptr("und"), + }, + IdpLinks: []*user.IDPLink{ + {IdpId: jwtIdPID, UserId: "id"}, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}}, + }, + }, + }, + wantErr: false, + }, + { + name: "retrieve successful jwt intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: jwtSuccessfulWithUserID, + IdpIntentToken: jwtWithUserToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(jwtWithUserChangeDate), + ResourceOwner: Instance.ID(), + Sequence: jwtWithUserSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: jwtIdPID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + }) + require.NoError(t, err) + return s + }(), + }, + UserId: "user", + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -3138,6 +3313,33 @@ func TestServer_CreateInviteCode(t *testing.T) { }, }, }, + { + name: "recreate", + args: args{ + ctx: CTX, + req: &user.CreateInviteCodeRequest{}, + prepare: func(request *user.CreateInviteCodeRequest) error { + resp := Instance.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + _, err := Instance.Client.UserV2.CreateInviteCode(CTX, &user.CreateInviteCodeRequest{ + UserId: resp.GetUserId(), + Verification: &user.CreateInviteCodeRequest_SendCode{ + SendCode: &user.SendInviteCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + ApplicationName: gu.Ptr("TestApp"), + }, + }, + }) + return err + }, + }, + want: &user.CreateInviteCodeResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Instance.DefaultOrg.Id, + }, + }, + }, { name: "create, return code, ok", args: args{ @@ -3419,7 +3621,6 @@ func TestServer_HumanMFAInitSkipped(t *testing.T) { t.Run(tt.name, func(t *testing.T) { err := tt.args.prepare(tt.args.req) require.NoError(t, err) - got, err := Client.HumanMFAInitSkipped(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) @@ -3433,3 +3634,1678 @@ func TestServer_HumanMFAInitSkipped(t *testing.T) { }) } } + +func TestServer_CreateUser(t *testing.T) { + type args struct { + ctx context.Context + req *user.CreateUserRequest + } + type testCase struct { + args args + want *user.CreateUserResponse + wantErr bool + } + tests := []struct { + name string + testCase func(runId string) testCase + }{ + { + name: "default verification", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + wantErr: false, + } + }, + }, + { + name: "return email verification code", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + EmailCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "return phone verification code", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + Phone: &user.SetHumanPhone{ + Phone: "+41791234567", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + PhoneCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template error", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing REQUIRED profile", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing REQUIRED email", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing empty email", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{}, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing idp", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + IdpLinks: []*user.IDPLink{ + { + IdpId: "idpID", + UserId: "userID", + UserName: "username", + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "with idp", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + IdpLinks: []*user.IDPLink{ + { + IdpId: idpResp.Id, + UserId: "userID", + UserName: "username", + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "with totp", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + TotpSecret: gu.Ptr("secret"), + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "password not complexity conform", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + NickName: gu.Ptr("Dukkie"), + DisplayName: gu.Ptr("Donald Duck"), + PreferredLanguage: gu.Ptr("en"), + Gender: user.Gender_GENDER_DIVERSE.Enum(), + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + PasswordType: &user.CreateUserRequest_Human_Password{ + Password: &user.Password{ + Password: "insufficient", + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "hashed password", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + PasswordType: &user.CreateUserRequest_Human_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm", + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "unsupported hashed password", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + PasswordType: &user.CreateUserRequest_Human_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ", + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "human default username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "machine user", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "machine default username to generated id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "machine default username to given id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + UserId: &runId, + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: runId, + }, + } + }, + }, + { + name: "org does not exist human, error", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: "does not exist", + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "org does not exist machine, error", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: "does not exist", + Username: &username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + test := tt.testCase(runId) + got, err := Client.CreateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + if test.want.GetEmailCode() != "" { + assert.NotEmpty(t, got.GetEmailCode(), "email code is empty") + } else { + assert.Empty(t, got.GetEmailCode(), "email code is not empty") + } + if test.want.GetPhoneCode() != "" { + assert.NotEmpty(t, got.GetPhoneCode(), "phone code is empty") + } else { + assert.Empty(t, got.GetPhoneCode(), "phone code is not empty") + } + if test.want.GetId() == "is generated" { + assert.Len(t, got.GetId(), 18, "ID is not 18 characters") + } else { + assert.Equal(t, test.want.GetId(), got.GetId(), "ID is not the same") + } + }) + } +} + +func TestServer_CreateUser_And_Compare(t *testing.T) { + type args struct { + ctx context.Context + req *user.CreateUserRequest + } + type testCase struct { + name string + args args + assert func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) + } + tests := []struct { + name string + testCase func(runId string) testCase + }{{ + name: "human given username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "human username default to email", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, email, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username given", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username default to generated id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, createResponse.GetId(), getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username default to given id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, runId, getResponse.GetUser().GetUsername()) + }, + } + }, + }} + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + test := tt.testCase(runId) + createResponse, err := Client.CreateUser(test.args.ctx, test.args.req) + require.NoError(t, err) + Instance.TriggerUserByID(test.args.ctx, createResponse.GetId()) + getResponse, err := Client.GetUserByID(test.args.ctx, &user.GetUserByIDRequest{ + UserId: createResponse.GetId(), + }) + require.NoError(t, err) + test.assert(t, createResponse, getResponse) + }) + } +} + +func TestServer_CreateUser_Permission(t *testing.T) { + newOrgOwnerEmail := gofakeit.Email() + newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) + type args struct { + ctx context.Context + req *user.CreateUserRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "human system, ok", + args: args{ + SystemCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + }, + { + name: "human instance, ok", + args: args{ + IamCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + }, + { + name: "human org, error", + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "human user, error", + args: args{ + UserCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "machine system, ok", + args: args{ + SystemCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + }, + { + name: "machine instance, ok", + args: args{ + IamCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + }, + { + name: "machine org, error", + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "machine user, error", + args: args{ + UserCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + wantErr: true, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID := fmt.Sprint(time.Now().UnixNano() + int64(i)) + tt.args.req.UserId = &userID + if email := tt.args.req.GetHuman().GetEmail(); email != nil { + email.Email = fmt.Sprintf("%s@example.com", userID) + } + _, err := Client.CreateUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestServer_UpdateUserTypeHuman(t *testing.T) { + type args struct { + ctx context.Context + req *user.UpdateUserRequest + } + type testCase struct { + args args + want *user.UpdateUserResponse + wantErr bool + } + tests := []struct { + name string + testCase func(runId, userId string) testCase + }{ + { + name: "default verification", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{}, + wantErr: false, + } + }, + }, + { + name: "return email verification code", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{ + EmailCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{}, + } + }, + }, + { + name: "return phone verification code", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Phone: "+41791234568", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{ + PhoneCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template error", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing empty email", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{}, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "password not complexity conform", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_Password{ + Password: &user.Password{ + Password: "insufficient", + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "hashed password", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm", + }, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{}, + } + }, + }, + { + name: "unsupported hashed password", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ", + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "update human user with machine fields, error", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: &runId, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + userId := Instance.CreateUserTypeHuman(CTX).GetId() + test := tt.testCase(runId, userId) + got, err := Client.UpdateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + assert.Less(t, changeDate, time.Now(), "change date is in the future") + if test.want.GetEmailCode() != "" { + assert.NotEmpty(t, got.GetEmailCode(), "email code is empty") + } else { + assert.Empty(t, got.GetEmailCode(), "email code is not empty") + } + if test.want.GetPhoneCode() != "" { + assert.NotEmpty(t, got.GetPhoneCode(), "phone code is empty") + } else { + assert.Empty(t, got.GetPhoneCode(), "phone code is not empty") + } + }) + } +} + +func TestServer_UpdateUserTypeMachine(t *testing.T) { + type args struct { + ctx context.Context + req *user.UpdateUserRequest + } + type testCase struct { + args args + wantErr bool + } + tests := []struct { + name string + testCase func(runId, userId string) testCase + }{ + { + name: "update machine, ok", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("donald"), + }, + }, + }, + }, + } + }, + }, + { + name: "update machine user with human fields, error", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + userId := Instance.CreateUserTypeMachine(CTX).GetId() + test := tt.testCase(runId, userId) + got, err := Client.UpdateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + assert.Less(t, changeDate, time.Now(), "change date is in the future") + }) + } +} + +func TestServer_UpdateUser_And_Compare(t *testing.T) { + type args struct { + ctx context.Context + create *user.CreateUserRequest + update *user.UpdateUserRequest + } + type testCase struct { + args args + assert func(t *testing.T, getResponse *user.GetUserByIDResponse) + } + tests := []struct { + name string + testCase func(runId string) testCase + }{{ + name: "human remove phone", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + Phone: &user.SetHumanPhone{ + Phone: "+1234567890", + }, + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{}, + }, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Empty(t, getResponse.GetUser().GetHuman().GetPhone().GetPhone(), "phone is not empty") + }, + } + }, + }, { + name: "human username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + Username: &username, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{}, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + ctx: CTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "Donald", + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + Username: &username, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{}, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }} + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + test := tt.testCase(runId) + createResponse, err := Client.CreateUser(test.args.ctx, test.args.create) + require.NoError(t, err) + _, err = Client.UpdateUser(test.args.ctx, test.args.update) + require.NoError(t, err) + Instance.TriggerUserByID(test.args.ctx, createResponse.GetId()) + getResponse, err := Client.GetUserByID(test.args.ctx, &user.GetUserByIDRequest{ + UserId: createResponse.GetId(), + }) + require.NoError(t, err) + test.assert(t, getResponse) + }) + } +} + +func TestServer_UpdateUser_Permission(t *testing.T) { + newOrgOwnerEmail := gofakeit.Email() + newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) + newHumanUserID := newOrg.CreatedAdmins[0].GetUserId() + machineUserResp, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: newOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "Donald", + }, + }, + }) + require.NoError(t, err) + newMachineUserID := machineUserResp.GetId() + Instance.TriggerUserByID(IamCTX, newMachineUserID) + type args struct { + ctx context.Context + req *user.UpdateUserRequest + } + type testCase struct { + args args + wantErr bool + } + tests := []struct { + name string + testCase func() testCase + }{ + { + name: "human, system, ok", + testCase: func() testCase { + return testCase{ + args: args{ + SystemCTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + } + }, + }, + { + name: "human instance, ok", + testCase: func() testCase { + return testCase{ + args: args{ + IamCTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + } + }, + }, + { + name: "human org, error", + testCase: func() testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "human user, error", + testCase: func() testCase { + return testCase{ + args: args{ + UserCTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "machine system, ok", + testCase: func() testCase { + return testCase{ + args: args{ + SystemCTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + } + }, + }, + { + name: "machine instance, ok", + testCase: func() testCase { + return testCase{ + args: args{ + IamCTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + } + }, + }, + { + name: "machine org, error", + testCase: func() testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "machine user, error", + testCase: func() testCase { + return testCase{ + args: args{ + UserCTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + test := tt.testCase() + _, err := Client.UpdateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go index 6e46dfd5c3..5514b6ef03 100644 --- a/internal/api/grpc/user/v2/intent.go +++ b/internal/api/grpc/user/v2/intent.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "time" oidc_pkg "github.com/zitadel/oidc/v3/pkg/oidc" "google.golang.org/protobuf/types/known/structpb" @@ -71,14 +72,14 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti if err != nil { return nil, err } - externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) + externalUser, userID, session, 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) + token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, session) if err != nil { return nil, err } @@ -116,7 +117,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse return "", nil } -func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { +func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, *ldap.Session, error) { provider, err := s.command.GetProvider(ctx, idpID, "", "") if err != nil { return nil, "", nil, err @@ -137,12 +138,7 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string 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 + return externalUser, userID, session, nil } func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { @@ -156,6 +152,9 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R if intent.State != domain.IDPIntentStateSucceeded { return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") } + if time.Now().After(intent.ExpiresAt()) { + return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-SAf42", "Errors.Intent.Expired") + } idpIntent, err := idpIntentToIDPIntentPb(intent, s.idpAlg) if err != nil { return nil, err @@ -168,21 +167,21 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R var idpUser idp.User switch p := provider.(type) { case *apple.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, &apple.User{}) + idpUser, err = unmarshalIdpUser(intent.IDPUser, apple.InitUser()) case *oauth.Provider: idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User()) case *oidc.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}) + idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser()) case *jwt.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, &jwt.User{}) + idpUser, err = unmarshalIdpUser(intent.IDPUser, jwt.InitUser()) 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{}}) + idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser()) case *google.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}) + idpUser, err = unmarshalIdpUser(intent.IDPUser, google.InitUser()) case *saml.Provider: idpUser, err = unmarshalIdpUser(intent.IDPUser, &saml.UserMapper{}) case *ldap.Provider: diff --git a/internal/api/grpc/user/v2/key.go b/internal/api/grpc/user/v2/key.go new file mode 100644 index 0000000000..59dab44248 --- /dev/null +++ b/internal/api/grpc/user/v2/key.go @@ -0,0 +1,62 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddKey(ctx context.Context, req *user.AddKeyRequest) (*user.AddKeyResponse, error) { + newMachineKey := &command.MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + ExpirationDate: req.GetExpirationDate().AsTime(), + Type: domain.AuthNKeyTypeJSON, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + } + newMachineKey.PublicKey = req.PublicKey + + pubkeySupplied := len(newMachineKey.PublicKey) > 0 + details, err := s.command.AddUserMachineKey(ctx, newMachineKey) + if err != nil { + return nil, err + } + // Return key details only if the pubkey wasn't supplied, otherwise the user already has + // private key locally + var keyDetails []byte + if !pubkeySupplied { + var err error + keyDetails, err = newMachineKey.Detail() + if err != nil { + return nil, err + } + } + return &user.AddKeyResponse{ + KeyId: newMachineKey.KeyID, + KeyContent: keyDetails, + CreationDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) RemoveKey(ctx context.Context, req *user.RemoveKeyRequest) (*user.RemoveKeyResponse, error) { + machineKey := &command.MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + KeyID: req.KeyId, + } + objectDetails, err := s.command.RemoveUserMachineKey(ctx, machineKey) + if err != nil { + return nil, err + } + return &user.RemoveKeyResponse{ + DeletionDate: timestamppb.New(objectDetails.EventDate), + }, nil +} diff --git a/internal/api/grpc/user/v2/key_query.go b/internal/api/grpc/user/v2/key_query.go new file mode 100644 index 0000000000..da4f47decf --- /dev/null +++ b/internal/api/grpc/user/v2/key_query.go @@ -0,0 +1,124 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) ListKeys(ctx context.Context, req *user.ListKeysRequest) (*user.ListKeysResponse, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + + filters, err := keyFiltersToQueries(req.Filters) + if err != nil { + return nil, err + } + search := &query.AuthNKeySearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: authnKeyFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: filters, + } + result, err := s.query.SearchAuthNKeys(ctx, search, query.JoinFilterUserMachine, s.checkPermission) + if err != nil { + return nil, err + } + resp := &user.ListKeysResponse{ + Result: make([]*user.Key, len(result.AuthNKeys)), + Pagination: filter.QueryToPaginationPb(search.SearchRequest, result.SearchResponse), + } + for i, key := range result.AuthNKeys { + resp.Result[i] = &user.Key{ + CreationDate: timestamppb.New(key.CreationDate), + ChangeDate: timestamppb.New(key.ChangeDate), + Id: key.ID, + UserId: key.AggregateID, + OrganizationId: key.ResourceOwner, + ExpirationDate: timestamppb.New(key.Expiration), + } + } + return resp, nil +} + +func keyFiltersToQueries(filters []*user.KeysSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(filters)) + for i, filter := range filters { + q[i], err = keyFilterToQuery(filter) + if err != nil { + return nil, err + } + } + return q, nil +} + +func keyFilterToQuery(filter *user.KeysSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *user.KeysSearchFilter_CreatedDateFilter: + return authnKeyCreatedFilterToQuery(q.CreatedDateFilter) + case *user.KeysSearchFilter_ExpirationDateFilter: + return authnKeyExpirationFilterToQuery(q.ExpirationDateFilter) + case *user.KeysSearchFilter_KeyIdFilter: + return authnKeyIdFilterToQuery(q.KeyIdFilter) + case *user.KeysSearchFilter_UserIdFilter: + return authnKeyUserIdFilterToQuery(q.UserIdFilter) + case *user.KeysSearchFilter_OrganizationIdFilter: + return authnKeyOrgIdFilterToQuery(q.OrganizationIdFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func authnKeyIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyIDQuery(f.Id) +} + +func authnKeyUserIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyIdentifyerQuery(f.Id) +} + +func authnKeyOrgIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyResourceOwnerQuery(f.Id) +} + +func authnKeyCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyCreationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +func authnKeyExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +// authnKeyFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination +func authnKeyFieldNameToSortingColumn(field *user.KeyFieldName) query.Column { + if field == nil { + return query.AuthNKeyColumnCreationDate + } + switch *field { + case user.KeyFieldName_KEY_FIELD_NAME_UNSPECIFIED: + return query.AuthNKeyColumnCreationDate + case user.KeyFieldName_KEY_FIELD_NAME_ID: + return query.AuthNKeyColumnID + case user.KeyFieldName_KEY_FIELD_NAME_USER_ID: + return query.AuthNKeyColumnIdentifier + case user.KeyFieldName_KEY_FIELD_NAME_ORGANIZATION_ID: + return query.AuthNKeyColumnResourceOwner + case user.KeyFieldName_KEY_FIELD_NAME_CREATED_DATE: + return query.AuthNKeyColumnCreationDate + case user.KeyFieldName_KEY_FIELD_NAME_KEY_EXPIRATION_DATE: + return query.AuthNKeyColumnExpiration + default: + return query.AuthNKeyColumnCreationDate + } +} diff --git a/internal/api/grpc/user/v2/machine.go b/internal/api/grpc/user/v2/machine.go new file mode 100644 index 0000000000..ad02b2289e --- /dev/null +++ b/internal/api/grpc/user/v2/machine.go @@ -0,0 +1,59 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.CreateUserRequest_Machine, orgId, userName, userId string) (*user.CreateUserResponse, error) { + cmd := &command.Machine{ + Username: userName, + Name: machinePb.Name, + Description: machinePb.GetDescription(), + AccessTokenType: domain.OIDCTokenTypeBearer, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: orgId, + AggregateID: userId, + }, + } + details, err := s.command.AddMachine( + ctx, + cmd, + nil, + s.command.NewPermissionCheckUserWrite(ctx), + command.AddMachineWithUsernameToIDFallback(), + ) + if err != nil { + return nil, err + } + return &user.CreateUserResponse{ + Id: cmd.AggregateID, + CreationDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) updateUserTypeMachine(ctx context.Context, machinePb *user.UpdateUserRequest_Machine, userId string, userName *string) (*user.UpdateUserResponse, error) { + cmd := updateMachineUserToCommand(userId, userName, machinePb) + err := s.command.ChangeUserMachine(ctx, cmd) + if err != nil { + return nil, err + } + return &user.UpdateUserResponse{ + ChangeDate: timestamppb.New(cmd.Details.EventDate), + }, nil +} + +func updateMachineUserToCommand(userId string, userName *string, machine *user.UpdateUserRequest_Machine) *command.ChangeMachine { + return &command.ChangeMachine{ + ID: userId, + Username: userName, + Name: machine.Name, + Description: machine.Description, + } +} diff --git a/internal/api/grpc/user/v2/machine_test.go b/internal/api/grpc/user/v2/machine_test.go new file mode 100644 index 0000000000..96d77d8fa2 --- /dev/null +++ b/internal/api/grpc/user/v2/machine_test.go @@ -0,0 +1,62 @@ +package user + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/muhlemmer/gu" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func Test_patchMachineUserToCommand(t *testing.T) { + type args struct { + userId string + userName *string + machine *user.UpdateUserRequest_Machine + } + tests := []struct { + name string + args args + want *command.ChangeMachine + }{{ + name: "single property", + args: args{ + userId: "userId", + machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("name"), + }, + }, + want: &command.ChangeMachine{ + ID: "userId", + Name: gu.Ptr("name"), + }, + }, { + name: "all properties", + args: args{ + userId: "userId", + userName: gu.Ptr("userName"), + machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("name"), + Description: gu.Ptr("description"), + }, + }, + want: &command.ChangeMachine{ + ID: "userId", + Username: gu.Ptr("userName"), + Name: gu.Ptr("name"), + Description: gu.Ptr("description"), + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateMachineUserToCommand(tt.args.userId, tt.args.userName, tt.args.machine) + if diff := cmp.Diff(tt.want, got, cmpopts.EquateComparable(language.Tag{})); diff != "" { + t.Errorf("patchMachineUserToCommand() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/api/grpc/user/v2/pat.go b/internal/api/grpc/user/v2/pat.go new file mode 100644 index 0000000000..54f6e99367 --- /dev/null +++ b/internal/api/grpc/user/v2/pat.go @@ -0,0 +1,56 @@ +package user + +import ( + "context" + + "github.com/zitadel/oidc/v3/pkg/oidc" + "google.golang.org/protobuf/types/known/timestamppb" + + z_oidc "github.com/zitadel/zitadel/internal/api/oidc" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddPersonalAccessToken(ctx context.Context, req *user.AddPersonalAccessTokenRequest) (*user.AddPersonalAccessTokenResponse, error) { + newPat := &command.PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + ExpirationDate: req.ExpirationDate.AsTime(), + Scopes: []string{ + oidc.ScopeOpenID, + oidc.ScopeProfile, + z_oidc.ScopeUserMetaData, + z_oidc.ScopeResourceOwner, + }, + AllowedUserType: domain.UserTypeMachine, + } + details, err := s.command.AddPersonalAccessToken(ctx, newPat) + if err != nil { + return nil, err + } + return &user.AddPersonalAccessTokenResponse{ + CreationDate: timestamppb.New(details.EventDate), + TokenId: newPat.TokenID, + Token: newPat.Token, + }, nil +} + +func (s *Server) RemovePersonalAccessToken(ctx context.Context, req *user.RemovePersonalAccessTokenRequest) (*user.RemovePersonalAccessTokenResponse, error) { + objectDetails, err := s.command.RemovePersonalAccessToken(ctx, &command.PersonalAccessToken{ + TokenID: req.TokenId, + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + }) + if err != nil { + return nil, err + } + return &user.RemovePersonalAccessTokenResponse{ + DeletionDate: timestamppb.New(objectDetails.EventDate), + }, nil +} diff --git a/internal/api/grpc/user/v2/pat_query.go b/internal/api/grpc/user/v2/pat_query.go new file mode 100644 index 0000000000..6bbd44d511 --- /dev/null +++ b/internal/api/grpc/user/v2/pat_query.go @@ -0,0 +1,123 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *user.ListPersonalAccessTokensRequest) (*user.ListPersonalAccessTokensResponse, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + filters, err := patFiltersToQueries(req.Filters) + if err != nil { + return nil, err + } + search := &query.PersonalAccessTokenSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: authnPersonalAccessTokenFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: filters, + } + result, err := s.query.SearchPersonalAccessTokens(ctx, search, s.checkPermission) + if err != nil { + return nil, err + } + resp := &user.ListPersonalAccessTokensResponse{ + Result: make([]*user.PersonalAccessToken, len(result.PersonalAccessTokens)), + Pagination: filter.QueryToPaginationPb(search.SearchRequest, result.SearchResponse), + } + for i, pat := range result.PersonalAccessTokens { + resp.Result[i] = &user.PersonalAccessToken{ + CreationDate: timestamppb.New(pat.CreationDate), + ChangeDate: timestamppb.New(pat.ChangeDate), + Id: pat.ID, + UserId: pat.UserID, + OrganizationId: pat.ResourceOwner, + ExpirationDate: timestamppb.New(pat.Expiration), + } + } + return resp, nil +} + +func patFiltersToQueries(filters []*user.PersonalAccessTokensSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(filters)) + for i, filter := range filters { + q[i], err = patFilterToQuery(filter) + if err != nil { + return nil, err + } + } + return q, nil +} + +func patFilterToQuery(filter *user.PersonalAccessTokensSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *user.PersonalAccessTokensSearchFilter_CreatedDateFilter: + return authnPersonalAccessTokenCreatedFilterToQuery(q.CreatedDateFilter) + case *user.PersonalAccessTokensSearchFilter_ExpirationDateFilter: + return authnPersonalAccessTokenExpirationFilterToQuery(q.ExpirationDateFilter) + case *user.PersonalAccessTokensSearchFilter_TokenIdFilter: + return authnPersonalAccessTokenIdFilterToQuery(q.TokenIdFilter) + case *user.PersonalAccessTokensSearchFilter_UserIdFilter: + return authnPersonalAccessTokenUserIdFilterToQuery(q.UserIdFilter) + case *user.PersonalAccessTokensSearchFilter_OrganizationIdFilter: + return authnPersonalAccessTokenOrgIdFilterToQuery(q.OrganizationIdFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func authnPersonalAccessTokenIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenIDQuery(f.Id) +} + +func authnPersonalAccessTokenUserIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenUserIDSearchQuery(f.Id) +} + +func authnPersonalAccessTokenOrgIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenResourceOwnerSearchQuery(f.Id) +} + +func authnPersonalAccessTokenCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenCreationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +func authnPersonalAccessTokenExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +// authnPersonalAccessTokenFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination +func authnPersonalAccessTokenFieldNameToSortingColumn(field *user.PersonalAccessTokenFieldName) query.Column { + if field == nil { + return query.PersonalAccessTokenColumnCreationDate + } + switch *field { + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_UNSPECIFIED: + return query.PersonalAccessTokenColumnCreationDate + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID: + return query.PersonalAccessTokenColumnID + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_USER_ID: + return query.PersonalAccessTokenColumnUserID + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ORGANIZATION_ID: + return query.PersonalAccessTokenColumnResourceOwner + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE: + return query.PersonalAccessTokenColumnCreationDate + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE: + return query.PersonalAccessTokenColumnExpiration + default: + return query.PersonalAccessTokenColumnCreationDate + } +} diff --git a/internal/api/grpc/user/v2/secret.go b/internal/api/grpc/user/v2/secret.go new file mode 100644 index 0000000000..1d54e1dde8 --- /dev/null +++ b/internal/api/grpc/user/v2/secret.go @@ -0,0 +1,39 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddSecret(ctx context.Context, req *user.AddSecretRequest) (*user.AddSecretResponse, error) { + newSecret := &command.GenerateMachineSecret{ + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + } + details, err := s.command.GenerateMachineSecret(ctx, req.UserId, "", newSecret) + if err != nil { + return nil, err + } + return &user.AddSecretResponse{ + CreationDate: timestamppb.New(details.EventDate), + ClientSecret: newSecret.ClientSecret, + }, nil +} + +func (s *Server) RemoveSecret(ctx context.Context, req *user.RemoveSecretRequest) (*user.RemoveSecretResponse, error) { + details, err := s.command.RemoveMachineSecret( + ctx, + req.UserId, + "", + s.command.NewPermissionCheckUserWrite(ctx), + ) + if err != nil { + return nil, err + } + return &user.RemoveSecretResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, nil +} diff --git a/internal/api/grpc/user/v2/server.go b/internal/api/grpc/user/v2/server.go index 9272ea27ee..e3c7e8011e 100644 --- a/internal/api/grpc/user/v2/server.go +++ b/internal/api/grpc/user/v2/server.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -18,12 +19,13 @@ var _ user.UserServiceServer = (*Server)(nil) type Server struct { user.UnimplementedUserServiceServer - command *command.Commands - query *query.Queries - userCodeAlg crypto.EncryptionAlgorithm - idpAlg crypto.EncryptionAlgorithm - idpCallback func(ctx context.Context) string - samlRootURL func(ctx context.Context, idpID string) string + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + userCodeAlg crypto.EncryptionAlgorithm + idpAlg crypto.EncryptionAlgorithm + idpCallback func(ctx context.Context) string + samlRootURL func(ctx context.Context, idpID string) string assetAPIPrefix func(context.Context) string @@ -33,6 +35,7 @@ type Server struct { type Config struct{} func CreateServer( + systemDefaults systemdefaults.SystemDefaults, command *command.Commands, query *query.Queries, userCodeAlg crypto.EncryptionAlgorithm, @@ -43,6 +46,7 @@ func CreateServer( checkPermission domain.PermissionCheck, ) *Server { return &Server{ + systemDefaults: systemDefaults, command: command, query: query, userCodeAlg: userCodeAlg, diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 0f958f0d40..6b4b2da75b 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -11,6 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) @@ -117,7 +118,7 @@ func genderToDomain(gender user.Gender) domain.Gender { } func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserRequest) (_ *user.UpdateHumanUserResponse, err error) { - human, err := UpdateUserRequestToChangeHuman(req) + human, err := updateHumanUserRequestToChangeHuman(req) if err != nil { return nil, err } @@ -181,86 +182,6 @@ func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p { return &pVal } -func UpdateUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) { - email, err := SetHumanEmailToEmail(req.Email, req.GetUserId()) - if err != nil { - return nil, err - } - return &command.ChangeHuman{ - ID: req.GetUserId(), - Username: req.Username, - Profile: SetHumanProfileToProfile(req.Profile), - Email: email, - Phone: SetHumanPhoneToPhone(req.Phone), - Password: SetHumanPasswordToPassword(req.Password), - }, nil -} - -func SetHumanProfileToProfile(profile *user.SetHumanProfile) *command.Profile { - if profile == nil { - return nil - } - var firstName *string - if profile.GivenName != "" { - firstName = &profile.GivenName - } - var lastName *string - if profile.FamilyName != "" { - lastName = &profile.FamilyName - } - return &command.Profile{ - FirstName: firstName, - LastName: lastName, - NickName: profile.NickName, - DisplayName: profile.DisplayName, - PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make), - Gender: ifNotNilPtr(profile.Gender, genderToDomain), - } -} - -func SetHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) { - if email == nil { - return nil, nil - } - var urlTemplate string - if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil { - urlTemplate = *email.GetSendCode().UrlTemplate - if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil { - return nil, err - } - } - return &command.Email{ - Address: domain.EmailAddress(email.Email), - Verified: email.GetIsVerified(), - ReturnCode: email.GetReturnCode() != nil, - URLTemplate: urlTemplate, - }, nil -} - -func SetHumanPhoneToPhone(phone *user.SetHumanPhone) *command.Phone { - if phone == nil { - return nil - } - return &command.Phone{ - Number: domain.PhoneNumber(phone.GetPhone()), - Verified: phone.GetIsVerified(), - ReturnCode: phone.GetReturnCode() != nil, - } -} - -func SetHumanPasswordToPassword(password *user.SetPassword) *command.Password { - if password == nil { - return nil - } - return &command.Password{ - PasswordCode: password.GetVerificationCode(), - OldPassword: password.GetCurrentPassword(), - Password: password.GetPassword().GetPassword(), - EncodedPasswordHash: password.GetHashedPassword().GetHash(), - ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(), - } -} - func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) { memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId()) if err != nil { @@ -482,3 +403,25 @@ func (s *Server) HumanMFAInitSkipped(ctx context.Context, req *user.HumanMFAInit Details: object.DomainToDetailsPb(details), }, nil } + +func (s *Server) CreateUser(ctx context.Context, req *user.CreateUserRequest) (*user.CreateUserResponse, error) { + switch userType := req.GetUserType().(type) { + case *user.CreateUserRequest_Human_: + return s.createUserTypeHuman(ctx, userType.Human, req.OrganizationId, req.Username, req.UserId) + case *user.CreateUserRequest_Machine_: + return s.createUserTypeMachine(ctx, userType.Machine, req.OrganizationId, req.GetUsername(), req.GetUserId()) + default: + return nil, zerrors.ThrowInternal(nil, "", "user type is not implemented") + } +} + +func (s *Server) UpdateUser(ctx context.Context, req *user.UpdateUserRequest) (*user.UpdateUserResponse, error) { + switch userType := req.GetUserType().(type) { + case *user.UpdateUserRequest_Human_: + return s.updateUserTypeHuman(ctx, userType.Human, req.UserId, req.Username) + case *user.UpdateUserRequest_Machine_: + return s.updateUserTypeMachine(ctx, userType.Machine, req.UserId, req.Username) + default: + return nil, zerrors.ThrowUnimplemented(nil, "", "user type is not implemented") + } +} diff --git a/internal/api/grpc/user/v2/query.go b/internal/api/grpc/user/v2/user_query.go similarity index 99% rename from internal/api/grpc/user/v2/query.go rename to internal/api/grpc/user/v2/user_query.go index 136a4a0932..dc886462be 100644 --- a/internal/api/grpc/user/v2/query.go +++ b/internal/api/grpc/user/v2/user_query.go @@ -298,11 +298,11 @@ func phoneQueryToQuery(q *user.PhoneQuery) (query.SearchQuery, error) { } func stateQueryToQuery(q *user.StateQuery) (query.SearchQuery, error) { - return query.NewUserStateSearchQuery(int32(q.State)) + return query.NewUserStateSearchQuery(q.State.ToDomain()) } func typeQueryToQuery(q *user.TypeQuery) (query.SearchQuery, error) { - return query.NewUserTypeSearchQuery(int32(q.Type)) + return query.NewUserTypeSearchQuery(q.Type.ToDomain()) } func loginNameQueryToQuery(q *user.LoginNameQuery) (query.SearchQuery, error) { 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 a81de58761..250322d66f 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -1706,12 +1706,11 @@ func TestServer_ReactivateUser(t *testing.T) { } func TestServer_DeleteUser(t *testing.T) { - projectResp, err := Instance.CreateProject(CTX) - require.NoError(t, err) + projectResp := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) type args struct { ctx context.Context req *user.DeleteUserRequest - prepare func(request *user.DeleteUserRequest) error + prepare func(request *user.DeleteUserRequest) } tests := []struct { name string @@ -1726,7 +1725,7 @@ func TestServer_DeleteUser(t *testing.T) { &user.DeleteUserRequest{ UserId: "notexisting", }, - func(request *user.DeleteUserRequest) error { return nil }, + nil, }, wantErr: true, }, @@ -1735,10 +1734,9 @@ func TestServer_DeleteUser(t *testing.T) { args: args{ ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(request *user.DeleteUserRequest) { resp := Instance.CreateHumanUser(CTX) request.UserId = resp.GetUserId() - return err }, }, want: &user.DeleteUserResponse{ @@ -1753,10 +1751,9 @@ func TestServer_DeleteUser(t *testing.T) { args: args{ ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(request *user.DeleteUserRequest) { resp := Instance.CreateMachineUser(CTX) request.UserId = resp.GetUserId() - return err }, }, want: &user.DeleteUserResponse{ @@ -1771,13 +1768,12 @@ func TestServer_DeleteUser(t *testing.T) { args: args{ ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(request *user.DeleteUserRequest) { resp := Instance.CreateHumanUser(CTX) request.UserId = resp.GetUserId() Instance.CreateProjectUserGrant(t, CTX, projectResp.GetId(), request.UserId) Instance.CreateProjectMembership(t, CTX, projectResp.GetId(), request.UserId) Instance.CreateOrgMembership(t, CTX, request.UserId) - return err }, }, want: &user.DeleteUserResponse{ @@ -1790,8 +1786,9 @@ func TestServer_DeleteUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.args.prepare(tt.args.req) - require.NoError(t, err) + if tt.args.prepare != nil { + tt.args.prepare(tt.args.req) + } got, err := Client.DeleteUser(tt.args.ctx, tt.args.req) if tt.wantErr { @@ -2153,22 +2150,36 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) require.NoError(t, err) intentID := authURL.Query().Get("state") + expiry := time.Now().Add(1 * time.Hour) + expiryFormatted := expiry.Round(time.Millisecond).UTC().Format("2006-01-02T15:04:05.999Z07:00") - successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "") + intentUser := Instance.CreateHumanUser(IamCTX) + _, err = Instance.CreateUserIDPlink(IamCTX, intentUser.GetUserId(), "idpUserID", oauthIdpID, "username") require.NoError(t, err) - successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user") + + successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry) require.NoError(t, err) - oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "") + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry) require.NoError(t, err) - oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user") + successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second)) + require.NoError(t, err) + // make sure the intent is expired + time.Sleep(2 * time.Second) + successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry) + require.NoError(t, err) + // make sure the intent is consumed + Instance.CreateIntentSession(t, IamCTX, intentUser.GetUserId(), successfulConsumedID, consumedToken) + oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "", expiry) + require.NoError(t, err) + oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user", expiry) 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", "") + samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry) require.NoError(t, err) - samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user") + samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry) require.NoError(t, err) type args struct { ctx context.Context @@ -2281,6 +2292,28 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful expired intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulExpiredID, + IdpIntentToken: expiredToken, + }, + }, + wantErr: true, + }, + { + name: "retrieve successful consumed intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulConsumedID, + IdpIntentToken: consumedToken, + }, + }, + wantErr: true, + }, { name: "retrieve successful oidc intent", args: args{ @@ -2466,7 +2499,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, IdpId: samlIdpID, @@ -2504,7 +2537,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, IdpId: samlIdpID, diff --git a/internal/api/grpc/user/v2beta/query.go b/internal/api/grpc/user/v2beta/query.go index e3602abc33..46b009a72e 100644 --- a/internal/api/grpc/user/v2beta/query.go +++ b/internal/api/grpc/user/v2beta/query.go @@ -292,11 +292,11 @@ func phoneQueryToQuery(q *user.PhoneQuery) (query.SearchQuery, error) { } func stateQueryToQuery(q *user.StateQuery) (query.SearchQuery, error) { - return query.NewUserStateSearchQuery(int32(q.State)) + return query.NewUserStateSearchQuery(q.State.ToDomain()) } func typeQueryToQuery(q *user.TypeQuery) (query.SearchQuery, error) { - return query.NewUserTypeSearchQuery(int32(q.Type)) + return query.NewUserTypeSearchQuery(q.Type.ToDomain()) } func loginNameQueryToQuery(q *user.LoginNameQuery) (query.SearchQuery, error) { diff --git a/internal/api/grpc/user/v2beta/user.go b/internal/api/grpc/user/v2beta/user.go index cf6dfa6304..93afbde0aa 100644 --- a/internal/api/grpc/user/v2beta/user.go +++ b/internal/api/grpc/user/v2beta/user.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "time" "golang.org/x/text/language" "google.golang.org/protobuf/types/known/structpb" @@ -399,14 +400,14 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti if err != nil { return nil, err } - externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) + externalUser, userID, session, 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) + token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, session) if err != nil { return nil, err } @@ -444,7 +445,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse return "", nil } -func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { +func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, *ldap.Session, error) { provider, err := s.command.GetProvider(ctx, idpID, "", "") if err != nil { return nil, "", nil, err @@ -470,7 +471,7 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string for _, item := range session.Entry.Attributes { attributes[item.Name] = item.Values } - return externalUser, userID, attributes, nil + return externalUser, userID, session, nil } func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { @@ -484,6 +485,9 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R if intent.State != domain.IDPIntentStateSucceeded { return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") } + if time.Now().After(intent.ExpiresAt()) { + return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-Afb2s", "Errors.Intent.Expired") + } return idpIntentToIDPIntentPb(intent, s.idpAlg) } diff --git a/internal/api/http/middleware/origin_interceptor.go b/internal/api/http/middleware/origin_interceptor.go index 35af8770b7..607855b80f 100644 --- a/internal/api/http/middleware/origin_interceptor.go +++ b/internal/api/http/middleware/origin_interceptor.go @@ -10,12 +10,12 @@ import ( http_util "github.com/zitadel/zitadel/internal/api/http" ) -func WithOrigin(fallBackToHttps bool, http1Header, http2Header string, instanceHostHeaders, publicDomainHeaders []string) mux.MiddlewareFunc { +func WithOrigin(enforceHttps bool, http1Header, http2Header string, instanceHostHeaders, publicDomainHeaders []string) mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := composeDomainContext( r, - fallBackToHttps, + enforceHttps, // to make sure we don't break existing configurations we append the existing checked headers as well slices.Compact(append(instanceHostHeaders, http1Header, http2Header, http_util.Forwarded, http_util.ZitadelForwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto)), publicDomainHeaders, @@ -25,28 +25,32 @@ func WithOrigin(fallBackToHttps bool, http1Header, http2Header string, instanceH } } -func composeDomainContext(r *http.Request, fallBackToHttps bool, instanceDomainHeaders, publicDomainHeaders []string) *http_util.DomainCtx { +func composeDomainContext(r *http.Request, enforceHttps bool, instanceDomainHeaders, publicDomainHeaders []string) *http_util.DomainCtx { instanceHost, instanceProto := hostFromRequest(r, instanceDomainHeaders) publicHost, publicProto := hostFromRequest(r, publicDomainHeaders) - if publicProto == "" { - publicProto = instanceProto - } - if publicProto == "" { - publicProto = "http" - if fallBackToHttps { - publicProto = "https" - } - } if instanceHost == "" { instanceHost = r.Host } return &http_util.DomainCtx{ InstanceHost: instanceHost, - Protocol: publicProto, + Protocol: protocolFromRequest(instanceProto, publicProto, enforceHttps), PublicHost: publicHost, } } +func protocolFromRequest(instanceProto, publicProto string, enforceHttps bool) string { + if enforceHttps { + return "https" + } + if publicProto != "" { + return publicProto + } + if instanceProto != "" { + return instanceProto + } + return "http" +} + func hostFromRequest(r *http.Request, headers []string) (host, proto string) { var hostFromHeader, protoFromHeader string for _, header := range headers { @@ -65,7 +69,7 @@ func hostFromRequest(r *http.Request, headers []string) (host, proto string) { if host == "" { host = hostFromHeader } - if proto == "" { + if proto == "" && (protoFromHeader == "http" || protoFromHeader == "https") { proto = protoFromHeader } } diff --git a/internal/api/http/middleware/origin_interceptor_test.go b/internal/api/http/middleware/origin_interceptor_test.go index 989e4d48b3..7419c91aba 100644 --- a/internal/api/http/middleware/origin_interceptor_test.go +++ b/internal/api/http/middleware/origin_interceptor_test.go @@ -11,8 +11,8 @@ import ( func Test_composeOrigin(t *testing.T) { type args struct { - h http.Header - fallBackToHttps bool + h http.Header + enforceHttps bool } tests := []struct { name string @@ -30,7 +30,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=https"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", @@ -42,7 +42,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"host=forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -54,7 +54,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=https;host=forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -66,7 +66,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=https;host=forwarded.host, proto=http;host=forwarded.host2"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -78,7 +78,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=https;host=forwarded.host, proto=http"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -90,11 +90,11 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=http", "proto=https;host=forwarded.host", "proto=http"}, }, - fallBackToHttps: true, + enforceHttps: true, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", - Protocol: "http", + Protocol: "https", }, }, { name: "x-forwarded-proto https", @@ -102,7 +102,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "X-Forwarded-Proto": []string{"https"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", @@ -114,25 +114,25 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "X-Forwarded-Proto": []string{"http"}, }, - fallBackToHttps: true, + enforceHttps: true, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", - Protocol: "http", + Protocol: "https", }, }, { name: "fallback to http", args: args{ - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", Protocol: "http", }, }, { - name: "fallback to https", + name: "enforce https", args: args{ - fallBackToHttps: true, + enforceHttps: true, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", @@ -144,7 +144,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "X-Forwarded-Host": []string{"x-forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "x-forwarded.host", @@ -157,7 +157,7 @@ func Test_composeOrigin(t *testing.T) { "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Host": []string{"x-forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "x-forwarded.host", @@ -170,7 +170,7 @@ func Test_composeOrigin(t *testing.T) { "Forwarded": []string{"host=forwarded.host"}, "X-Forwarded-Host": []string{"x-forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -183,7 +183,7 @@ func Test_composeOrigin(t *testing.T) { "Forwarded": []string{"host=forwarded.host"}, "X-Forwarded-Proto": []string{"https"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -198,10 +198,10 @@ func Test_composeOrigin(t *testing.T) { Host: "host.header", Header: tt.args.h, }, - tt.args.fallBackToHttps, + tt.args.enforceHttps, []string{http_util.Forwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto}, []string{"x-zitadel-public-host"}, - ), "headers: %+v, fallBackToHttps: %t", tt.args.h, tt.args.fallBackToHttps) + ), "headers: %+v, enforceHttps: %t", tt.args.h, tt.args.enforceHttps) }) } } diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index c3e9586a59..8b1c24134a 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -3,6 +3,7 @@ package idp import ( "bytes" "context" + "encoding/base64" "encoding/xml" "errors" "fmt" @@ -15,10 +16,13 @@ import ( "github.com/muhlemmer/gu" "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/ui/login" + "github.com/zitadel/zitadel/internal/cache" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain/federatedlogout" "github.com/zitadel/zitadel/internal/form" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/apple" @@ -44,6 +48,8 @@ const ( metadataPath = idpPrefix + "/saml/metadata" acsPath = idpPrefix + "/saml/acs" certificatePath = idpPrefix + "/saml/certificate" + sloPath = idpPrefix + "/saml/slo" + jwtPath = "/jwt" paramIntentID = "id" paramToken = "token" @@ -62,6 +68,7 @@ type Handler struct { callbackURL func(ctx context.Context) string samlRootURL func(ctx context.Context, idpID string) string loginSAMLRootURL func(ctx context.Context) string + caches *Caches } type externalIDPCallbackData struct { @@ -104,6 +111,7 @@ func NewHandler( queries *query.Queries, encryptionAlgorithm crypto.EncryptionAlgorithm, instanceInterceptor func(next http.Handler) http.Handler, + federatedLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout], ) http.Handler { h := &Handler{ commands: commands, @@ -113,6 +121,7 @@ func NewHandler( callbackURL: CallbackURL(), samlRootURL: SAMLRootURL(), loginSAMLRootURL: LoginSAMLRootURL(), + caches: &Caches{federatedLogouts: federatedLogoutCache}, } router := mux.NewRouter() @@ -121,9 +130,15 @@ func NewHandler( router.HandleFunc(metadataPath, h.handleMetadata) router.HandleFunc(certificatePath, h.handleCertificate) router.HandleFunc(acsPath, h.handleACS) + router.HandleFunc(sloPath, h.handleSLO) + router.HandleFunc(jwtPath, h.handleJWT) return router } +type Caches struct { + federatedLogouts cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout] +} + func parseSAMLRequest(r *http.Request) *externalSAMLIDPCallbackData { vars := mux.Vars(r) return &externalSAMLIDPCallbackData{ @@ -287,7 +302,7 @@ func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) { userID, err := h.checkExternalUser(ctx, intent.IDPID, idpUser.GetID()) logging.WithFields("intent", intent.AggregateID).OnError(err).Error("could not check if idp user already exists") - token, err := h.commands.SucceedSAMLIDPIntent(ctx, intent, idpUser, userID, session.Assertion) + token, err := h.commands.SucceedSAMLIDPIntent(ctx, intent, idpUser, userID, session) if err != nil { redirectToFailureURLErr(w, r, intent, zerrors.ThrowInternal(err, "IDP-JdD3g", "Errors.Intent.TokenCreationFailed")) return @@ -295,6 +310,89 @@ func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) { redirectToSuccessURL(w, r, intent, token, userID) } +func (h *Handler) handleJWT(w http.ResponseWriter, r *http.Request) { + intentID, err := h.intentIDFromJWTRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + intent, err := h.commands.GetActiveIntent(r.Context(), intentID) + if err != nil { + if zerrors.IsNotFound(err) { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + redirectToFailureURLErr(w, r, intent, err) + return + } + idpConfig, err := h.getProvider(r.Context(), intent.IDPID) + if err != nil { + cmdErr := h.commands.FailIDPIntent(r.Context(), intent, err.Error()) + logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") + redirectToFailureURLErr(w, r, intent, err) + return + } + jwtIDP, ok := idpConfig.(*jwt.Provider) + if !ok { + err := zerrors.ThrowInvalidArgument(nil, "IDP-JK23ed", "Errors.ExternalIDP.IDPTypeNotImplemented") + cmdErr := h.commands.FailIDPIntent(r.Context(), intent, err.Error()) + logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") + redirectToFailureURLErr(w, r, intent, err) + return + } + h.handleJWTExtraction(w, r, intent, jwtIDP) +} + +func (h *Handler) intentIDFromJWTRequest(r *http.Request) (string, error) { + // for compatibility of the old JWT provider we use the auth request id parameter to pass the intent id + intentID := r.FormValue(jwt.QueryAuthRequestID) + // for compatibility of the old JWT provider we use the user agent id parameter to pass the encrypted intent id + encryptedIntentID := r.FormValue(jwt.QueryUserAgentID) + if err := h.checkIntentID(intentID, encryptedIntentID); err != nil { + return "", err + } + return intentID, nil +} + +func (h *Handler) checkIntentID(intentID, encryptedIntentID string) error { + if intentID == "" || encryptedIntentID == "" { + return zerrors.ThrowInvalidArgument(nil, "LOGIN-adfzz", "Errors.AuthRequest.MissingParameters") + } + id, err := base64.RawURLEncoding.DecodeString(encryptedIntentID) + if err != nil { + return err + } + decryptedIntentID, err := h.encryptionAlgorithm.DecryptString(id, h.encryptionAlgorithm.EncryptionKeyID()) + if err != nil { + return err + } + if intentID != decryptedIntentID { + return zerrors.ThrowInvalidArgument(nil, "LOGIN-adfzz", "Errors.AuthRequest.MissingParameters") + } + return nil +} + +func (h *Handler) handleJWTExtraction(w http.ResponseWriter, r *http.Request, intent *command.IDPIntentWriteModel, identityProvider *jwt.Provider) { + session := jwt.NewSessionFromRequest(identityProvider, r) + user, err := session.FetchUser(r.Context()) + if err != nil { + cmdErr := h.commands.FailIDPIntent(r.Context(), intent, err.Error()) + logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") + redirectToFailureURLErr(w, r, intent, err) + return + } + + userID, err := h.checkExternalUser(r.Context(), intent.IDPID, user.GetID()) + logging.WithFields("intent", intent.AggregateID).OnError(err).Error("could not check if idp user already exists") + + token, err := h.commands.SucceedIDPIntent(r.Context(), intent, user, session, userID) + if err != nil { + redirectToFailureURLErr(w, r, intent, err) + return + } + redirectToSuccessURL(w, r, intent, token, userID) +} + func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) { ctx := r.Context() data, err := h.parseCallbackRequest(r) @@ -351,6 +449,38 @@ func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) { redirectToSuccessURL(w, r, intent, token, userID) } +func (h *Handler) handleSLO(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + data := parseSAMLRequest(r) + + logoutState, ok := h.caches.federatedLogouts.Get(ctx, federatedlogout.IndexRequestID, federatedlogout.Key(authz.GetInstance(ctx).InstanceID(), data.RelayState)) + if !ok || logoutState.State != federatedlogout.StateRedirected { + err := zerrors.ThrowNotFound(nil, "SAML-3uor2", "Errors.Intent.NotFound") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // For the moment we just make sure the callback matches the IDP it was started on / intended for. + + provider, err := h.getProvider(ctx, data.IDPID) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if _, ok = provider.(*saml2.Provider); !ok { + err := zerrors.ThrowInvalidArgument(nil, "SAML-ui9wyux0hp", "Errors.Intent.IDPInvalid") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // We could also parse and validate the response here, but for example Azure does not sign it and thus would already fail. + // Also we can't really act on it if it fails. + + err = h.caches.federatedLogouts.Delete(ctx, federatedlogout.IndexRequestID, federatedlogout.Key(logoutState.InstanceID, logoutState.SessionID)) + logging.WithFields("instanceID", logoutState.InstanceID, "sessionID", logoutState.SessionID).OnError(err).Error("could not delete federated logout") + http.Redirect(w, r, logoutState.PostLogoutRedirectURI, http.StatusFound) +} + func (h *Handler) tryMigrateExternalUser(ctx context.Context, idpID string, idpUser idp.User, idpSession idp.Session) (userID string, err error) { migration, ok := idpSession.(idp.SessionSupportsMigration) if !ok { diff --git a/internal/api/idp/idp_test.go b/internal/api/idp/idp_test.go index 6804a035af..2f64f598a9 100644 --- a/internal/api/idp/idp_test.go +++ b/internal/api/idp/idp_test.go @@ -4,6 +4,7 @@ import ( "net/http/httptest" "net/url" "testing" + "time" "github.com/stretchr/testify/assert" @@ -14,11 +15,12 @@ import ( func Test_redirectToSuccessURL(t *testing.T) { type args struct { - id string - userID string - token string - failureURL string - successURL string + id string + userID string + token string + failureURL string + successURL string + maxIdPIntentLifetime time.Duration } type res struct { want string @@ -59,7 +61,7 @@ func Test_redirectToSuccessURL(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) resp := httptest.NewRecorder() - wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id) + wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime) wm.FailureURL, _ = url.Parse(tt.args.failureURL) wm.SuccessURL, _ = url.Parse(tt.args.successURL) @@ -71,11 +73,12 @@ func Test_redirectToSuccessURL(t *testing.T) { func Test_redirectToFailureURL(t *testing.T) { type args struct { - id string - failureURL string - successURL string - err string - desc string + id string + failureURL string + successURL string + err string + desc string + maxIdPIntentLifetime time.Duration } type res struct { want string @@ -115,7 +118,7 @@ func Test_redirectToFailureURL(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) resp := httptest.NewRecorder() - wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id) + wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime) wm.FailureURL, _ = url.Parse(tt.args.failureURL) wm.SuccessURL, _ = url.Parse(tt.args.successURL) @@ -127,10 +130,11 @@ func Test_redirectToFailureURL(t *testing.T) { func Test_redirectToFailureURLErr(t *testing.T) { type args struct { - id string - failureURL string - successURL string - err error + id string + failureURL string + successURL string + err error + maxIdPIntentLifetime time.Duration } type res struct { want string @@ -158,7 +162,7 @@ func Test_redirectToFailureURLErr(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) resp := httptest.NewRecorder() - wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id) + wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime) wm.FailureURL, _ = url.Parse(tt.args.failureURL) wm.SuccessURL, _ = url.Parse(tt.args.successURL) diff --git a/internal/api/oidc/access_token.go b/internal/api/oidc/access_token.go index 66da6e3ccf..2f2880efc2 100644 --- a/internal/api/oidc/access_token.go +++ b/internal/api/oidc/access_token.go @@ -122,7 +122,7 @@ func (s *Server) assertClientScopesForPAT(ctx context.Context, token *accessToke if err != nil { return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } - roles, err := s.query.SearchProjectRoles(ctx, authz.GetFeatures(ctx).TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) + roles, err := s.query.SearchProjectRoles(ctx, authz.GetFeatures(ctx).TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, nil) if err != nil { return err } diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index f750b2a3ea..da0e084877 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -21,6 +21,7 @@ import ( "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/handler" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/domain/federatedlogout" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/user/model" @@ -236,6 +237,11 @@ func (o *OPStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken } func (o *OPStorage) TerminateSession(ctx context.Context, userID, clientID string) (err error) { + _, err = o.terminateSession(ctx, userID) + return err +} + +func (o *OPStorage) terminateSession(ctx context.Context, userID string) (sessions []command.HumanSignOutSession, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { err = oidcError(err) @@ -244,22 +250,22 @@ func (o *OPStorage) TerminateSession(ctx context.Context, userID, clientID strin userAgentID, ok := middleware.UserAgentIDFromCtx(ctx) if !ok { logging.Error("no user agent id") - return zerrors.ThrowPreconditionFailed(nil, "OIDC-fso7F", "no user agent id") + return nil, zerrors.ThrowPreconditionFailed(nil, "OIDC-fso7F", "no user agent id") } - sessions, err := o.repo.UserSessionsByAgentID(ctx, userAgentID) + sessions, err = o.repo.UserSessionsByAgentID(ctx, userAgentID) if err != nil { logging.WithError(err).Error("error retrieving user sessions") - return err + return nil, err } if len(sessions) == 0 { - return nil + return nil, nil } data := authz.CtxData{ UserID: userID, } err = o.command.HumansSignOut(authz.SetCtxData(ctx, data), userAgentID, sessions) logging.OnError(err).Error("error signing out") - return err + return sessions, err } func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionRequest *op.EndSessionRequest) (redirectURI string, err error) { @@ -294,7 +300,16 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR // So if any condition is not met, we handle the request as a V1 request and do a (v1) TerminateSession, // which terminates all sessions of the user agent, identified by cookie. if endSessionRequest.IDTokenHintClaims == nil || endSessionRequest.IDTokenHintClaims.SessionID == "" { - return endSessionRequest.RedirectURI, o.TerminateSession(ctx, endSessionRequest.UserID, endSessionRequest.ClientID) + sessions, err := o.terminateSession(ctx, endSessionRequest.UserID) + if err != nil { + return "", err + } + if len(sessions) == 1 { + if path := o.federatedLogout(ctx, sessions[0].ID, endSessionRequest.RedirectURI); path != "" { + return path, nil + } + } + return endSessionRequest.RedirectURI, nil } // V1: @@ -305,6 +320,9 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR if err != nil { return "", err } + if path := o.federatedLogout(ctx, endSessionRequest.IDTokenHintClaims.SessionID, endSessionRequest.RedirectURI); path != "" { + return path, nil + } return endSessionRequest.RedirectURI, nil } @@ -317,6 +335,39 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR return v2PostLogoutRedirectURI(endSessionRequest.RedirectURI), nil } +// federatedLogout checks whether the session has an idp session linked and the IDP template is configured for federated logout. +// If so, it creates a federated logout request and stores it in the cache and returns the logout path. +func (o *OPStorage) federatedLogout(ctx context.Context, sessionID string, postLogoutRedirectURI string) string { + session, err := o.repo.UserSessionByID(ctx, sessionID) + if err != nil { + logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "sessionID", sessionID). + WithError(err).Error("error retrieving user session") + return "" + } + if session.SelectedIDPConfigID.String == "" { + return "" + } + identityProvider, err := o.query.IDPTemplateByID(ctx, false, session.SelectedIDPConfigID.String, false, nil) + if err != nil { + logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "idpID", session.SelectedIDPConfigID.String, "sessionID", sessionID). + WithError(err).Error("error retrieving idp template") + return "" + } + if identityProvider.SAMLIDPTemplate == nil || !identityProvider.FederatedLogoutEnabled { + return "" + } + o.federateLogoutCache.Set(ctx, &federatedlogout.FederatedLogout{ + InstanceID: authz.GetInstance(ctx).InstanceID(), + FingerPrintID: authz.GetCtxData(ctx).AgentID, + SessionID: sessionID, + IDPID: session.SelectedIDPConfigID.String, + UserID: session.UserID, + PostLogoutRedirectURI: postLogoutRedirectURI, + State: federatedlogout.StateCreated, + }) + return login.ExternalLogoutPath(sessionID) +} + func buildLoginV2LogoutURL(baseURI *url.URL, redirectURI string) string { baseURI.JoinPath(LogoutPath) q := baseURI.Query() @@ -459,7 +510,7 @@ func (o *OPStorage) assertProjectRoleScopes(ctx context.Context, clientID string if err != nil { return nil, zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } - roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) + roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, nil) if err != nil { return nil, err } @@ -482,7 +533,7 @@ func (o *OPStorage) assertProjectRoleScopesByProject(ctx context.Context, projec if err != nil { return nil, zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } - roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) + roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, nil) if err != nil { return nil, err } @@ -498,7 +549,7 @@ func (o *OPStorage) assertClientScopesForPAT(ctx context.Context, token *model.T if err != nil { return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } - roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) + roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, nil) if err != nil { return err } diff --git a/internal/api/oidc/error.go b/internal/api/oidc/error.go index 781a1d3815..829a15d4d2 100644 --- a/internal/api/oidc/error.go +++ b/internal/api/oidc/error.go @@ -42,10 +42,7 @@ func oidcError(err error) error { if statusCode < 500 { newOidcErr = oidc.ErrInvalidRequest } - return op.NewStatusError( - newOidcErr(). - WithParent(err). - WithDescription(zError.GetMessage()), - statusCode, - ) + oidcErr := newOidcErr().WithParent(err) + oidcErr.Description = zError.GetMessage() + return op.NewStatusError(oidcErr, statusCode) } diff --git a/internal/api/oidc/integration_test/client_test.go b/internal/api/oidc/integration_test/client_test.go index 43b000108c..08c17f69cb 100644 --- a/internal/api/oidc/integration_test/client_test.go +++ b/internal/api/oidc/integration_test/client_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v3/pkg/client" @@ -24,8 +25,7 @@ import ( ) func TestServer_Introspect(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) app, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) @@ -188,10 +188,8 @@ func assertIntrospection( // with clients that have different authentication methods. func TestServer_VerifyClient(t *testing.T) { sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) - projectInactive, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) + projectInactive := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) inactiveClient, err := Instance.CreateOIDCInactivateClient(CTX, redirectURI, logoutRedirectURI, project.GetId()) require.NoError(t, err) diff --git a/internal/api/oidc/integration_test/oidc_test.go b/internal/api/oidc/integration_test/oidc_test.go index 2ab78b972e..8bb103d0eb 100644 --- a/internal/api/oidc/integration_test/oidc_test.go +++ b/internal/api/oidc/integration_test/oidc_test.go @@ -421,21 +421,20 @@ type clientOpts struct { func createClientWithOpts(t testing.TB, instance *integration.Instance, opts clientOpts) (clientID, projectID string) { ctx := instance.WithAuthorization(CTX, integration.UserTypeOrgOwner) - project, err := instance.CreateProject(ctx) - require.NoError(t, err) + project := instance.CreateProject(ctx, t.(*testing.T), "", gofakeit.AppName(), false, false) app, err := instance.CreateOIDCClientLoginVersion(ctx, opts.redirectURI, opts.logoutURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, opts.devMode, opts.LoginVersion) require.NoError(t, err) return app.GetClientId(), project.GetId() } func createImplicitClient(t testing.TB) string { - app, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil) + app, err := Instance.CreateOIDCImplicitFlowClient(CTX, t.(*testing.T), redirectURIImplicit, nil) require.NoError(t, err) return app.GetClientId() } func createImplicitClientNoLoginClientHeader(t testing.TB) string { - app, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}}) + app, err := Instance.CreateOIDCImplicitFlowClient(CTX, t.(*testing.T), redirectURIImplicit, &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}}) require.NoError(t, err) return app.GetClientId() } diff --git a/internal/api/oidc/integration_test/token_device_test.go b/internal/api/oidc/integration_test/token_device_test.go index 0c6a65e8a2..9692909205 100644 --- a/internal/api/oidc/integration_test/token_device_test.go +++ b/internal/api/oidc/integration_test/token_device_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v3/pkg/client/rp" @@ -21,8 +22,7 @@ import ( ) func TestServer_DeviceAuth(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) 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) diff --git a/internal/api/oidc/integration_test/token_exchange_test.go b/internal/api/oidc/integration_test/token_exchange_test.go index 0844898a2f..dcd2d61669 100644 --- a/internal/api/oidc/integration_test/token_exchange_test.go +++ b/internal/api/oidc/integration_test/token_exchange_test.go @@ -147,7 +147,7 @@ func TestServer_TokenExchange(t *testing.T) { ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) userResp := instance.CreateHumanUser(ctx) - client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx) + client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx, t) require.NoError(t, err) signer, err := rp.SignerFromKeyFile(keyData)() require.NoError(t, err) @@ -371,7 +371,7 @@ func TestServer_TokenExchangeImpersonation(t *testing.T) { setTokenExchangeFeature(t, instance, true) setImpersonationPolicy(t, instance, true) - client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx) + client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx, t) require.NoError(t, err) signer, err := rp.SignerFromKeyFile(keyData)() require.NoError(t, err) @@ -591,7 +591,7 @@ func TestImpersonation_API_Call(t *testing.T) { instance := integration.NewInstance(CTX) ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx) + client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx, t) require.NoError(t, err) signer, err := rp.SignerFromKeyFile(keyData)() require.NoError(t, err) diff --git a/internal/api/oidc/integration_test/userinfo_test.go b/internal/api/oidc/integration_test/userinfo_test.go index da1dc6b1e3..d4aded0b48 100644 --- a/internal/api/oidc/integration_test/userinfo_test.go +++ b/internal/api/oidc/integration_test/userinfo_test.go @@ -309,9 +309,7 @@ func TestServer_UserInfo_Issue6662(t *testing.T) { roleBar = "bar" ) - project, err := Instance.CreateProject(CTX) - projectID := project.GetId() - require.NoError(t, err) + projectID := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false).GetId() user, _, clientID, clientSecret, err := Instance.CreateOIDCCredentialsClient(CTX) require.NoError(t, err) addProjectRolesGrants(t, user.GetUserId(), projectID, roleFoo, roleBar) diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 81f3b1c466..852bbc7db8 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -11,6 +11,7 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/jonboulle/clockwork" "github.com/muhlemmer/gu" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/op" @@ -350,14 +351,14 @@ func (o *OPStorage) getSigningKey(ctx context.Context) (op.SigningKey, error) { if len(keys.Keys) > 0 { return PrivateKeyToSigningKey(SelectSigningKey(keys.Keys), o.encAlg) } - var position float64 + var position decimal.Decimal if keys.State != nil { position = keys.State.Position } return nil, o.refreshSigningKey(ctx, position) } -func (o *OPStorage) refreshSigningKey(ctx context.Context, position float64) error { +func (o *OPStorage) refreshSigningKey(ctx context.Context, position decimal.Decimal) error { ok, err := o.ensureIsLatestKey(ctx, position) if err != nil || !ok { return zerrors.ThrowInternal(err, "OIDC-ASfh3", "cannot ensure that projection is up to date") @@ -369,12 +370,12 @@ func (o *OPStorage) refreshSigningKey(ctx context.Context, position float64) err return zerrors.ThrowInternal(nil, "OIDC-Df1bh", "") } -func (o *OPStorage) ensureIsLatestKey(ctx context.Context, position float64) (bool, error) { - maxSequence, err := o.getMaxKeySequence(ctx) +func (o *OPStorage) ensureIsLatestKey(ctx context.Context, position decimal.Decimal) (bool, error) { + maxSequence, err := o.getMaxKeyPosition(ctx) if err != nil { return false, fmt.Errorf("error retrieving new events: %w", err) } - return position >= maxSequence, nil + return position.GreaterThanOrEqual(maxSequence), nil } func PrivateKeyToSigningKey(key query.PrivateKey, algorithm crypto.EncryptionAlgorithm) (_ op.SigningKey, err error) { @@ -412,9 +413,9 @@ func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context) error { return o.command.GenerateSigningKeyPair(setOIDCCtx(ctx), "RS256") } -func (o *OPStorage) getMaxKeySequence(ctx context.Context) (float64, error) { - return o.eventstore.LatestSequence(ctx, - eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). +func (o *OPStorage) getMaxKeyPosition(ctx context.Context) (decimal.Decimal, error) { + return o.eventstore.LatestPosition(ctx, + eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). ResourceOwner(authz.GetInstance(ctx).InstanceID()). AwaitOpenTransactions(). AddQuery(). diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index 37a9ba2bce..d7171b957b 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -15,9 +15,11 @@ import ( "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/auth/repository" + "github.com/zitadel/zitadel/internal/cache" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain/federatedlogout" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/crdb" "github.com/zitadel/zitadel/internal/query" @@ -76,6 +78,7 @@ type OPStorage struct { locker crdb.Locker assetAPIPrefix func(ctx context.Context) string contextToIssuer func(context.Context) string + federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout] } // Provider is used to overload certain [op.Provider] methods @@ -115,12 +118,13 @@ func NewServer( accessHandler *middleware.AccessInterceptor, fallbackLogger *slog.Logger, hashConfig crypto.HashConfig, + federatedLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout], ) (*Server, error) { opConfig, err := createOPConfig(config, defaultLogoutRedirectURI, cryptoKey) if err != nil { return nil, zerrors.ThrowInternal(err, "OIDC-EGrqd", "cannot create op config: %w") } - storage := newStorage(config, command, query, repo, encryptionAlg, es, projections, ContextToIssuer) + storage := newStorage(config, command, query, repo, encryptionAlg, es, projections, ContextToIssuer, federatedLogoutCache) keyCache := newPublicKeyCache(ctx, config.PublicKeyCacheMaxAge, queryKeyFunc(query)) accessTokenKeySet := newOidcKeySet(keyCache, withKeyExpiryCheck(true)) idTokenHintKeySet := newOidcKeySet(keyCache) @@ -225,7 +229,17 @@ 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, contextToIssuer func(context.Context) string) *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, + federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout], +) *OPStorage { return &OPStorage{ repo: repo, command: command, @@ -242,6 +256,7 @@ func newStorage(config Config, command *command.Commands, query *query.Queries, locker: crdb.NewLocker(db.DB, locksTable, signingKey), assetAPIPrefix: assets.AssetAPI(), contextToIssuer: contextToIssuer, + federateLogoutCache: federateLogoutCache, } } diff --git a/internal/api/saml/certificate.go b/internal/api/saml/certificate.go index ff130f7709..14752cd5cd 100644 --- a/internal/api/saml/certificate.go +++ b/internal/api/saml/certificate.go @@ -6,6 +6,7 @@ import ( "time" "github.com/go-jose/go-jose/v4" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/saml/pkg/provider/key" @@ -76,7 +77,7 @@ func (p *Storage) getCertificateAndKey(ctx context.Context, usage crypto.KeyUsag return p.certificateToCertificateAndKey(selectCertificate(certs.Certificates)) } - var position float64 + var position decimal.Decimal if certs.State != nil { position = certs.State.Position } @@ -87,7 +88,7 @@ func (p *Storage) getCertificateAndKey(ctx context.Context, usage crypto.KeyUsag func (p *Storage) refreshCertificate( ctx context.Context, usage crypto.KeyUsage, - position float64, + position decimal.Decimal, ) error { ok, err := p.ensureIsLatestCertificate(ctx, position) if err != nil { @@ -103,12 +104,12 @@ func (p *Storage) refreshCertificate( return nil } -func (p *Storage) ensureIsLatestCertificate(ctx context.Context, position float64) (bool, error) { - maxSequence, err := p.getMaxKeySequence(ctx) +func (p *Storage) ensureIsLatestCertificate(ctx context.Context, position decimal.Decimal) (bool, error) { + maxSequence, err := p.getMaxKeyPosition(ctx) if err != nil { return false, fmt.Errorf("error retrieving new events: %w", err) } - return position >= maxSequence, nil + return position.GreaterThanOrEqual(maxSequence), nil } func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage crypto.KeyUsage) error { @@ -151,9 +152,9 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage cr } } -func (p *Storage) getMaxKeySequence(ctx context.Context) (float64, error) { - return p.eventstore.LatestSequence(ctx, - eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). +func (p *Storage) getMaxKeyPosition(ctx context.Context) (decimal.Decimal, error) { + return p.eventstore.LatestPosition(ctx, + eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). ResourceOwner(authz.GetInstance(ctx).InstanceID()). AwaitOpenTransactions(). AddQuery(). diff --git a/internal/api/scim/integration_test/users_delete_test.go b/internal/api/scim/integration_test/users_delete_test.go index 86e88f46c4..23c335f93c 100644 --- a/internal/api/scim/integration_test/users_delete_test.go +++ b/internal/api/scim/integration_test/users_delete_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" @@ -76,13 +77,12 @@ func TestDeleteUser_errors(t *testing.T) { func TestDeleteUser_ensureReallyDeleted(t *testing.T) { // create user and dependencies createUserResp := Instance.CreateHumanUser(CTX) - proj, err := Instance.CreateProject(CTX) - require.NoError(t, err) + proj := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) Instance.CreateProjectUserGrant(t, CTX, proj.Id, createUserResp.UserId) // delete user via scim - err = Instance.Client.SCIM.Users.Delete(CTX, Instance.DefaultOrg.Id, createUserResp.UserId) + err := Instance.Client.SCIM.Users.Delete(CTX, Instance.DefaultOrg.Id, createUserResp.UserId) assert.NoError(t, err) // ensure it is really deleted => try to delete again => should 404 diff --git a/internal/api/scim/resources/user.go b/internal/api/scim/resources/user.go index bc8d864994..13baed5d51 100644 --- a/internal/api/scim/resources/user.go +++ b/internal/api/scim/resources/user.go @@ -203,7 +203,6 @@ func (h *UsersHandler) Delete(ctx context.Context, id string) error { if err != nil { return err } - _, err = h.command.RemoveUserV2(ctx, id, authz.GetCtxData(ctx).OrgID, memberships, grants...) return err } diff --git a/internal/api/scim/resources/user_query_builder.go b/internal/api/scim/resources/user_query_builder.go index b86b171fb5..7a06e4b3fd 100644 --- a/internal/api/scim/resources/user_query_builder.go +++ b/internal/api/scim/resources/user_query_builder.go @@ -70,7 +70,7 @@ func (h *UsersHandler) buildListQuery(ctx context.Context, request *ListRequest) } // the zitadel scim implementation only supports humans for now - userTypeQuery, err := query.NewUserTypeSearchQuery(int32(domain.UserTypeHuman)) + userTypeQuery, err := query.NewUserTypeSearchQuery(domain.UserTypeHuman) if err != nil { return nil, err } diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index d198978f1a..6202c38c8b 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -3,11 +3,13 @@ package login import ( "context" "errors" + "html/template" "net/http" "net/url" "slices" "strings" + crewjam_saml "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/client/rp" @@ -20,6 +22,7 @@ import ( http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/domain/federatedlogout" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/apple" @@ -639,9 +642,10 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ } resourceOwner := determineResourceOwner(r.Context(), authReq) if orgIAMPolicy == nil { - orgIAMPolicy, err = l.getOrgDomainPolicy(r, resourceOwner) - if err != nil { - l.renderError(w, r, authReq, err) + var policyErr error + orgIAMPolicy, policyErr = l.getOrgDomainPolicy(r, resourceOwner) + if policyErr != nil { + l.renderError(w, r, authReq, policyErr) return } } @@ -652,19 +656,22 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ human, idpLink, _ = mapExternalUserToLoginUser(linkingUser, orgIAMPolicy.UserLoginMustBeDomain) } - labelPolicy, err := l.getLabelPolicy(r, resourceOwner) - if err != nil { - l.renderError(w, r, authReq, err) + labelPolicy, policyErr := l.getLabelPolicy(r, resourceOwner) + if policyErr != nil { + l.renderError(w, r, authReq, policyErr) return } - idpTemplate, err := l.getIDPByID(r, idpLink.IDPConfigID) - if err != nil { - l.renderError(w, r, authReq, err) + idpTemplate, idpErr := l.getIDPByID(r, idpLink.IDPConfigID) + if idpErr != nil { + l.renderError(w, r, authReq, idpErr) return } if !idpTemplate.IsCreationAllowed && !idpTemplate.IsLinkingAllowed { - l.renderError(w, r, authReq, zerrors.ThrowPreconditionFailed(nil, "LOGIN-3kl44", "Errors.User.ExternalIDP.NoOptionAllowed")) + if err == nil { + err = zerrors.ThrowPreconditionFailed(nil, "LOGIN-3kl44", "Errors.User.ExternalIDP.NoOptionAllowed") + } + l.renderError(w, r, authReq, err) return } @@ -1418,6 +1425,124 @@ func (l *Login) getUserLinks(ctx context.Context, userID, idpID string) (*query. ) } +type federatedLogoutData struct { + SessionID string `schema:"sessionID"` +} + +const ( + federatedLogoutDataSessionID = "sessionID" +) + +func ExternalLogoutPath(sessionID string) string { + v := url.Values{} + v.Set(federatedLogoutDataSessionID, sessionID) + return HandlerPrefix + EndpointExternalLogout + "?" + v.Encode() +} + +// handleExternalLogout is called when a user signed out of ZITADEL with a federated logout +func (l *Login) handleExternalLogout(w http.ResponseWriter, r *http.Request) { + data := new(federatedLogoutData) + err := l.parser.Parse(r, data) + if err != nil { + l.renderError(w, r, nil, err) + return + } + + logoutRequest, ok := l.caches.federatedLogouts.Get(r.Context(), federatedlogout.IndexRequestID, federatedlogout.Key(authz.GetInstance(r.Context()).InstanceID(), data.SessionID)) + if !ok || logoutRequest.State != federatedlogout.StateCreated || logoutRequest.FingerPrintID != authz.GetCtxData(r.Context()).AgentID { + l.renderError(w, r, nil, zerrors.ThrowNotFound(nil, "LOGIN-ADK21", "Errors.ExternalIDP.LogoutRequestNotFound")) + return + } + + provider, err := l.externalLogoutProvider(r, logoutRequest.IDPID) + if err != nil { + l.renderError(w, r, nil, err) + return + } + + nameID, err := l.externalUserID(r.Context(), logoutRequest.UserID, logoutRequest.IDPID) + if err != nil { + l.renderError(w, r, nil, err) + return + } + + err = samlLogoutRequest(w, r, provider, nameID, logoutRequest.SessionID) + if err != nil { + l.renderError(w, r, nil, err) + return + } + logoutRequest.State = federatedlogout.StateRedirected + l.caches.federatedLogouts.Set(r.Context(), logoutRequest) +} + +func (l *Login) externalLogoutProvider(r *http.Request, providerID string) (*saml.Provider, error) { + identityProvider, err := l.getIDPByID(r, providerID) + if err != nil { + return nil, err + } + if identityProvider.Type != domain.IDPTypeSAML { + return nil, zerrors.ThrowInvalidArgument(nil, "LOGIN-ADK21", "Errors.ExternalIDP.IDPTypeNotImplemented") + } + return l.samlProvider(r.Context(), identityProvider) +} + +func samlLogoutRequest(w http.ResponseWriter, r *http.Request, provider *saml.Provider, nameID, sessionID string) error { + mw, err := provider.GetSP() + if err != nil { + return err + } + // We ignore the configured binding and only check the available SLO endpoints from the metadata. + // For example, Azure documents that only redirect binding is possible and also only provides a redirect SLO in the metadata. + slo := mw.ServiceProvider.GetSLOBindingLocation(crewjam_saml.HTTPRedirectBinding) + if slo != "" { + return samlRedirectLogoutRequest(w, r, mw.ServiceProvider, slo, nameID, sessionID) + } + slo = mw.ServiceProvider.GetSLOBindingLocation(crewjam_saml.HTTPPostBinding) + return samlPostLogoutRequest(w, mw.ServiceProvider, slo, nameID, sessionID) +} + +func samlRedirectLogoutRequest(w http.ResponseWriter, r *http.Request, sp crewjam_saml.ServiceProvider, slo, nameID, sessionID string) error { + lr, err := sp.MakeLogoutRequest(slo, nameID) + if err != nil { + return err + } + http.Redirect(w, r, lr.Redirect(sessionID).String(), http.StatusFound) + return nil +} + +var ( + samlSLOPostTemplate = template.Must(template.New("samlSLOPost").Parse(`{{.Form}}`)) +) + +type samlSLOPostData struct { + Form template.HTML +} + +func samlPostLogoutRequest(w http.ResponseWriter, sp crewjam_saml.ServiceProvider, slo, nameID, sessionID string) error { + lr, err := sp.MakeLogoutRequest(slo, nameID) + if err != nil { + return err + } + + return samlSLOPostTemplate.Execute(w, &samlSLOPostData{Form: template.HTML(lr.Post(sessionID))}) +} + +func (l *Login) externalUserID(ctx context.Context, userID, idpID string) (string, error) { + userIDQuery, err := query.NewIDPUserLinksUserIDSearchQuery(userID) + if err != nil { + return "", err + } + idpIDQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID) + if err != nil { + return "", err + } + links, err := l.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: []query.SearchQuery{userIDQuery, idpIDQuery}}, nil) + if err != nil || len(links.Links) != 1 { + return "", zerrors.ThrowPreconditionFailed(err, "LOGIN-ADK21", "Errors.User.ExternalIDP.NotFound") + } + return links.Links[0].ProvidedUserID, nil +} + // IdPError wraps an error from an external IDP to be able to distinguish it from other errors and to display it // more prominent (popup style) . // It's used if an error occurs during the login process with an external IDP and local authentication is allowed, diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index 444c5aaa85..f1ce9bfa2a 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -21,6 +21,7 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/domain/federatedlogout" "github.com/zitadel/zitadel/internal/form" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/static" @@ -60,25 +61,20 @@ const ( DefaultLoggedOutPath = HandlerPrefix + EndpointLogoutDone ) -func CreateLogin(config Config, +func CreateLogin( + config Config, command *command.Commands, query *query.Queries, authRepo *eventsourcing.EsRepository, staticStorage static.Storage, consolePath string, - oidcAuthCallbackURL func(context.Context, string) string, - samlAuthCallbackURL func(context.Context, string) string, + oidcAuthCallbackURL, samlAuthCallbackURL func(context.Context, string) string, externalSecure bool, - userAgentCookie, - issuerInterceptor, - oidcInstanceHandler, - samlInstanceHandler, - assetCache, - accessHandler mux.MiddlewareFunc, - userCodeAlg crypto.EncryptionAlgorithm, - idpConfigAlg crypto.EncryptionAlgorithm, + userAgentCookie, issuerInterceptor, oidcInstanceHandler, samlInstanceHandler, assetCache, accessHandler mux.MiddlewareFunc, + userCodeAlg, idpConfigAlg crypto.EncryptionAlgorithm, csrfCookieKey []byte, cacheConnectors connector.Connectors, + federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout], ) (*Login, error) { login := &Login{ oidcAuthCallbackURL: oidcAuthCallbackURL, @@ -101,7 +97,7 @@ func CreateLogin(config Config, login.parser = form.NewParser() var err error - login.caches, err = startCaches(context.Background(), cacheConnectors) + login.caches, err = startCaches(context.Background(), cacheConnectors, federateLogoutCache) if err != nil { return nil, err } @@ -112,7 +108,11 @@ func csp() *middleware.CSP { csp := middleware.DefaultSCP csp.ObjectSrc = middleware.CSPSourceOptsSelf() csp.StyleSrc = csp.StyleSrc.AddNonce() - csp.ScriptSrc = csp.ScriptSrc.AddNonce().AddHash("sha256", "AjPdJSbZmeWHnEc5ykvJFay8FTWeTeRbs9dutfZ0HqE=") + csp.ScriptSrc = csp.ScriptSrc.AddNonce(). + // SAML POST ACS + AddHash("sha256", "AjPdJSbZmeWHnEc5ykvJFay8FTWeTeRbs9dutfZ0HqE="). + // SAML POST SLO + AddHash("sha256", "4Su6mBWzEIFnH4pAGMOuaeBrstwJN4Z3pq/s1Kn4/KQ=") return &csp } @@ -215,14 +215,16 @@ func (l *Login) baseURL(ctx context.Context) string { type Caches struct { idpFormCallbacks cache.Cache[idpFormCallbackIndex, string, *idpFormCallback] + federatedLogouts cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout] } -func startCaches(background context.Context, connectors connector.Connectors) (_ *Caches, err error) { +func startCaches(background context.Context, connectors connector.Connectors, federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout]) (_ *Caches, err error) { caches := new(Caches) caches.idpFormCallbacks, err = connector.StartCache[idpFormCallbackIndex, string, *idpFormCallback](background, []idpFormCallbackIndex{idpFormCallbackIndexRequestID}, cache.PurposeIdPFormCallback, connectors.Config.IdPFormCallbacks, connectors) if err != nil { return nil, err } + caches.federatedLogouts = federateLogoutCache return caches, nil } diff --git a/internal/api/ui/login/router.go b/internal/api/ui/login/router.go index 6e346c9da0..459a0aab5d 100644 --- a/internal/api/ui/login/router.go +++ b/internal/api/ui/login/router.go @@ -14,6 +14,7 @@ const ( EndpointExternalLogin = "/login/externalidp" EndpointExternalLoginCallback = "/login/externalidp/callback" EndpointExternalLoginCallbackFormPost = "/login/externalidp/callback/form" + EndpointExternalLogout = "/logout/externalidp" EndpointSAMLACS = "/login/externalidp/saml/acs" EndpointJWTAuthorize = "/login/jwt/authorize" EndpointJWTCallback = "/login/jwt/callback" @@ -77,6 +78,7 @@ func CreateRouter(login *Login, interceptors ...mux.MiddlewareFunc) *mux.Router router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet) router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet) router.HandleFunc(EndpointExternalLoginCallbackFormPost, login.handleExternalLoginCallbackForm).Methods(http.MethodPost) + router.HandleFunc(EndpointExternalLogout, login.handleExternalLogout).Methods(http.MethodGet) router.HandleFunc(EndpointSAMLACS, login.handleExternalLoginCallback).Methods(http.MethodGet) router.HandleFunc(EndpointSAMLACS, login.handleExternalLoginCallbackForm).Methods(http.MethodPost) router.HandleFunc(EndpointJWTAuthorize, login.handleJWTRequest).Methods(http.MethodGet) diff --git a/internal/api/ui/login/static/resources/scripts/copy_to_clipboard.js b/internal/api/ui/login/static/resources/scripts/copy_to_clipboard.js index 97359cdde5..848aa0742b 100644 --- a/internal/api/ui/login/static/resources/scripts/copy_to_clipboard.js +++ b/internal/api/ui/login/static/resources/scripts/copy_to_clipboard.js @@ -3,4 +3,4 @@ const copyToClipboard = str => { }; let copyButton = document.getElementById("copy"); -copyButton.addEventListener("click", copyToClipboard(copyButton.getAttribute("data-copy"))); +copyButton.addEventListener("click", () => copyToClipboard(copyButton.getAttribute("data-copy"))); diff --git a/internal/api/ui/login/static/templates/mfa_init_otp.html b/internal/api/ui/login/static/templates/mfa_init_otp.html index b84a88a5b1..a9ae31dcdd 100644 --- a/internal/api/ui/login/static/templates/mfa_init_otp.html +++ b/internal/api/ui/login/static/templates/mfa_init_otp.html @@ -28,7 +28,7 @@
{{.Secret}} -
diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 0ede13ae68..7c335a752f 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -125,7 +125,7 @@ type userGrantProvider interface { type projectProvider interface { ProjectByClientID(context.Context, string) (*query.Project, error) - SearchProjectGrants(ctx context.Context, queries *query.ProjectGrantSearchQueries) (projects *query.ProjectGrants, err error) + SearchProjectGrants(ctx context.Context, queries *query.ProjectGrantSearchQueries, permissionCheck domain.PermissionCheck) (projects *query.ProjectGrants, err error) } type applicationProvider interface { @@ -1841,7 +1841,7 @@ func projectRequired(ctx context.Context, request *domain.AuthRequest, projectPr if err != nil { return false, err } - grants, err := projectProvider.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectID, grantedOrg}}) + grants, err := projectProvider.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectID, grantedOrg}}, nil) if err != nil { return false, err } diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index 7d71ddecd9..78edd2a7e4 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -286,7 +286,7 @@ func (m *mockProject) ProjectByClientID(ctx context.Context, s string) (*query.P return &query.Project{ResourceOwner: m.resourceOwner, HasProjectCheck: m.projectCheck}, nil } -func (m *mockProject) SearchProjectGrants(ctx context.Context, queries *query.ProjectGrantSearchQueries) (*query.ProjectGrants, error) { +func (m *mockProject) SearchProjectGrants(ctx context.Context, queries *query.ProjectGrantSearchQueries, permissionCheck domain.PermissionCheck) (*query.ProjectGrants, error) { if m.hasProject { mockProjectGrant := new(query.ProjectGrant) return &query.ProjectGrants{ProjectGrants: []*query.ProjectGrant{mockProjectGrant}}, nil diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go index 61895c263d..6c5375a6b9 100644 --- a/internal/auth/repository/eventsourcing/eventstore/user.go +++ b/internal/auth/repository/eventsourcing/eventstore/user.go @@ -13,6 +13,7 @@ import ( "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/repository/user" usr_view "github.com/zitadel/zitadel/internal/user/repository/view" + "github.com/zitadel/zitadel/internal/user/repository/view/model" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -49,6 +50,10 @@ func (repo *UserRepo) UserAgentIDBySessionID(ctx context.Context, sessionID stri return repo.View.UserAgentIDBySessionID(ctx, sessionID, authz.GetInstance(ctx).InstanceID()) } +func (repo *UserRepo) UserSessionByID(ctx context.Context, sessionID string) (*model.UserSessionView, error) { + return repo.View.UserSessionByID(ctx, sessionID, authz.GetInstance(ctx).InstanceID()) +} + func (repo *UserRepo) ActiveUserSessionsBySessionID(ctx context.Context, sessionID string) (userAgentID string, signoutSessions []command.HumanSignOutSession, err error) { userAgentID, sessions, err := repo.View.ActiveUserSessionsBySessionID(ctx, sessionID, authz.GetInstance(ctx).InstanceID()) if err != nil { diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go index 0d87ab06bb..0c151bb412 100644 --- a/internal/auth/repository/eventsourcing/handler/handler.go +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -2,8 +2,14 @@ package handler import ( "context" + "errors" + "fmt" "time" + "github.com/jackc/pgx/v5/pgconn" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" @@ -72,11 +78,21 @@ func Projections() []*handler2.Handler { } func ProjectInstance(ctx context.Context) error { - for _, projection := range projections { - _, err := projection.Trigger(ctx) - if err != nil { - return err + for i, projection := range projections { + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting auth projection") + for { + _, err := projection.Trigger(ctx) + if err == nil { + break + } + var pgErr *pgconn.PgError + errors.As(err, &pgErr) + if pgErr.Code != database.PgUniqueConstraintErrorCode { + return err + } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("auth projection failed because of unique constraint, retrying") } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("auth projection done") } return nil } diff --git a/internal/auth/repository/eventsourcing/view/user_session.go b/internal/auth/repository/eventsourcing/view/user_session.go index a4618e11fb..777bc3213f 100644 --- a/internal/auth/repository/eventsourcing/view/user_session.go +++ b/internal/auth/repository/eventsourcing/view/user_session.go @@ -16,6 +16,10 @@ func (v *View) UserSessionByIDs(ctx context.Context, agentID, userID, instanceID return view.UserSessionByIDs(ctx, v.client, agentID, userID, instanceID) } +func (v *View) UserSessionByID(ctx context.Context, userSessionID, instanceID string) (*model.UserSessionView, error) { + return view.UserSessionByID(ctx, v.client, userSessionID, instanceID) +} + func (v *View) UserSessionsByAgentID(ctx context.Context, agentID, instanceID string) ([]*model.UserSessionView, error) { return view.UserSessionsByAgentID(ctx, v.client, agentID, instanceID) } diff --git a/internal/auth/repository/user.go b/internal/auth/repository/user.go index f09581b32e..b51ca27f24 100644 --- a/internal/auth/repository/user.go +++ b/internal/auth/repository/user.go @@ -4,10 +4,12 @@ import ( "context" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/user/repository/view/model" ) type UserRepository interface { UserSessionsByAgentID(ctx context.Context, agentID string) (sessions []command.HumanSignOutSession, err error) UserAgentIDBySessionID(ctx context.Context, sessionID string) (string, error) + UserSessionByID(ctx context.Context, sessionID string) (*model.UserSessionView, error) ActiveUserSessionsBySessionID(ctx context.Context, sessionID string) (userAgentID string, sessions []command.HumanSignOutSession, err error) } diff --git a/internal/cache/cache.go b/internal/cache/cache.go index dc05208caa..233308a3cb 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -18,6 +18,7 @@ const ( PurposeMilestones PurposeOrganization PurposeIdPFormCallback + PurposeFederatedLogout ) // Cache stores objects with a value of type `V`. diff --git a/internal/cache/connector/connector.go b/internal/cache/connector/connector.go index 1a0534759a..532e795a81 100644 --- a/internal/cache/connector/connector.go +++ b/internal/cache/connector/connector.go @@ -23,6 +23,7 @@ type CachesConfig struct { Milestones *cache.Config Organization *cache.Config IdPFormCallbacks *cache.Config + FederatedLogouts *cache.Config } type Connectors struct { diff --git a/internal/cache/purpose_enumer.go b/internal/cache/purpose_enumer.go index a93a978efb..f721435593 100644 --- a/internal/cache/purpose_enumer.go +++ b/internal/cache/purpose_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _PurposeName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback" +const _PurposeName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callbackfederated_logout" -var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47, 65} +var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47, 65, 81} -const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback" +const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callbackfederated_logout" func (i Purpose) String() string { if i < 0 || i >= Purpose(len(_PurposeIndex)-1) { @@ -29,9 +29,10 @@ func _PurposeNoOp() { _ = x[PurposeMilestones-(2)] _ = x[PurposeOrganization-(3)] _ = x[PurposeIdPFormCallback-(4)] + _ = x[PurposeFederatedLogout-(5)] } -var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization, PurposeIdPFormCallback} +var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization, PurposeIdPFormCallback, PurposeFederatedLogout} var _PurposeNameToValueMap = map[string]Purpose{ _PurposeName[0:11]: PurposeUnspecified, @@ -44,6 +45,8 @@ var _PurposeNameToValueMap = map[string]Purpose{ _PurposeLowerName[35:47]: PurposeOrganization, _PurposeName[47:65]: PurposeIdPFormCallback, _PurposeLowerName[47:65]: PurposeIdPFormCallback, + _PurposeName[65:81]: PurposeFederatedLogout, + _PurposeLowerName[65:81]: PurposeFederatedLogout, } var _PurposeNames = []string{ @@ -52,6 +55,7 @@ var _PurposeNames = []string{ _PurposeName[25:35], _PurposeName[35:47], _PurposeName[47:65], + _PurposeName[65:81], } // PurposeString retrieves an enum value from the enum constants string name. diff --git a/internal/command/command.go b/internal/command/command.go index b0e67ad52e..64b7b53b67 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -81,6 +81,7 @@ type Commands struct { publicKeyLifetime time.Duration certificateLifetime time.Duration defaultSecretGenerators *SecretGenerators + maxIdPIntentLifetime time.Duration samlCertificateAndKeyGenerator func(id string) ([]byte, []byte, error) webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error) @@ -152,6 +153,7 @@ func StartCommands( privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime, publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime, certificateLifetime: defaults.KeyConfig.CertificateLifetime, + maxIdPIntentLifetime: defaults.MaxIdPIntentLifetime, idpConfigEncryption: idpConfigEncryption, smtpEncryption: smtpEncryption, smsEncryption: smsEncryption, diff --git a/internal/command/idp.go b/internal/command/idp.go index 821a577900..06d8f473c0 100644 --- a/internal/command/idp.go +++ b/internal/command/idp.go @@ -122,6 +122,7 @@ type SAMLProvider struct { WithSignedRequest bool NameIDFormat *domain.SAMLNameIDFormat TransientMappingAttributeName string + FederatedLogoutEnabled bool IDPOptions idp.Options } diff --git a/internal/command/idp_intent.go b/internal/command/idp_intent.go index 3cd9991679..9690117edd 100644 --- a/internal/command/idp_intent.go +++ b/internal/command/idp_intent.go @@ -7,7 +7,6 @@ import ( "encoding/xml" "net/url" - "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -19,8 +18,10 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/apple" "github.com/zitadel/zitadel/internal/idp/providers/azuread" "github.com/zitadel/zitadel/internal/idp/providers/jwt" + "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" "github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -68,7 +69,7 @@ func (c *Commands) CreateIntent(ctx context.Context, intentID, idpID, successURL return nil, nil, err } } - writeModel := NewIDPIntentWriteModel(intentID, resourceOwner) + writeModel := NewIDPIntentWriteModel(intentID, resourceOwner, c.maxIdPIntentLifetime) //nolint: staticcheck cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL, idpArguments)) @@ -180,6 +181,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr userID, accessToken, idToken, + idpSession.ExpiresAt(), ) err = c.pushAppendAndReduce(ctx, writeModel, cmd) if err != nil { @@ -188,7 +190,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr return token, nil } -func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, assertion *saml.Assertion) (string, error) { +func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, session *saml.Session) (string, error) { token, err := c.generateIntentToken(writeModel.AggregateID) if err != nil { return "", err @@ -197,7 +199,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte if err != nil { return "", err } - assertionData, err := xml.Marshal(assertion) + assertionData, err := xml.Marshal(session.Assertion) if err != nil { return "", err } @@ -213,6 +215,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte idpUser.GetPreferredUsername(), userID, assertionEnc, + session.ExpiresAt(), ) err = c.pushAppendAndReduce(ctx, writeModel, cmd) if err != nil { @@ -237,7 +240,7 @@ func (c *Commands) generateIntentToken(intentID string) (string, error) { return base64.RawURLEncoding.EncodeToString(token), nil } -func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, attributes map[string][]string) (string, error) { +func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, session *ldap.Session) (string, error) { token, err := c.generateIntentToken(writeModel.AggregateID) if err != nil { return "", err @@ -246,6 +249,10 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte if err != nil { return "", err } + attributes := make(map[string][]string, len(session.Entry.Attributes)) + for _, item := range session.Entry.Attributes { + attributes[item.Name] = item.Values + } cmd := idpintent.NewLDAPSucceededEvent( ctx, IDPIntentAggregateFromWriteModel(&writeModel.WriteModel), @@ -254,6 +261,7 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte idpUser.GetPreferredUsername(), userID, attributes, + session.ExpiresAt(), ) err = c.pushAppendAndReduce(ctx, writeModel, cmd) if err != nil { @@ -273,7 +281,7 @@ func (c *Commands) FailIDPIntent(ctx context.Context, writeModel *IDPIntentWrite } func (c *Commands) GetIntentWriteModel(ctx context.Context, id, resourceOwner string) (*IDPIntentWriteModel, error) { - writeModel := NewIDPIntentWriteModel(id, resourceOwner) + writeModel := NewIDPIntentWriteModel(id, resourceOwner, c.maxIdPIntentLifetime) err := c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err diff --git a/internal/command/idp_intent_model.go b/internal/command/idp_intent_model.go index c6bc26ab06..07e0821813 100644 --- a/internal/command/idp_intent_model.go +++ b/internal/command/idp_intent_model.go @@ -2,6 +2,7 @@ package command import ( "net/url" + "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -29,18 +30,29 @@ type IDPIntentWriteModel struct { RequestID string Assertion *crypto.CryptoValue - State domain.IDPIntentState + State domain.IDPIntentState + succeededAt time.Time + maxIdPIntentLifetime time.Duration + expiresAt time.Time } -func NewIDPIntentWriteModel(id, resourceOwner string) *IDPIntentWriteModel { +func NewIDPIntentWriteModel(id, resourceOwner string, maxIdPIntentLifetime time.Duration) *IDPIntentWriteModel { return &IDPIntentWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: id, ResourceOwner: resourceOwner, }, + maxIdPIntentLifetime: maxIdPIntentLifetime, } } +func (wm *IDPIntentWriteModel) ExpiresAt() time.Time { + if wm.expiresAt.IsZero() { + return wm.succeededAt.Add(wm.maxIdPIntentLifetime) + } + return wm.expiresAt +} + func (wm *IDPIntentWriteModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { @@ -56,6 +68,8 @@ func (wm *IDPIntentWriteModel) Reduce() error { wm.reduceLDAPSucceededEvent(e) case *idpintent.FailedEvent: wm.reduceFailedEvent(e) + case *idpintent.ConsumedEvent: + wm.reduceConsumedEvent(e) } } return wm.WriteModel.Reduce() @@ -74,6 +88,7 @@ func (wm *IDPIntentWriteModel) Query() *eventstore.SearchQueryBuilder { idpintent.SAMLRequestEventType, idpintent.LDAPSucceededEventType, idpintent.FailedEventType, + idpintent.ConsumedEventType, ). Builder() } @@ -93,6 +108,8 @@ func (wm *IDPIntentWriteModel) reduceSAMLSucceededEvent(e *idpintent.SAMLSucceed wm.IDPUserName = e.IDPUserName wm.Assertion = e.Assertion wm.State = domain.IDPIntentStateSucceeded + wm.succeededAt = e.CreationDate() + wm.expiresAt = e.ExpiresAt } func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceededEvent) { @@ -102,6 +119,8 @@ func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceed wm.IDPUserName = e.IDPUserName wm.IDPEntryAttributes = e.EntryAttributes wm.State = domain.IDPIntentStateSucceeded + wm.succeededAt = e.CreationDate() + wm.expiresAt = e.ExpiresAt } func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededEvent) { @@ -112,6 +131,8 @@ func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededE wm.IDPAccessToken = e.IDPAccessToken wm.IDPIDToken = e.IDPIDToken wm.State = domain.IDPIntentStateSucceeded + wm.succeededAt = e.CreationDate() + wm.expiresAt = e.ExpiresAt } func (wm *IDPIntentWriteModel) reduceSAMLRequestEvent(e *idpintent.SAMLRequestEvent) { @@ -122,6 +143,10 @@ func (wm *IDPIntentWriteModel) reduceFailedEvent(e *idpintent.FailedEvent) { wm.State = domain.IDPIntentStateFailed } +func (wm *IDPIntentWriteModel) reduceConsumedEvent(e *idpintent.ConsumedEvent) { + wm.State = domain.IDPIntentStateConsumed +} + func IDPIntentAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { return &eventstore.Aggregate{ Type: idpintent.AggregateType, diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index 2400b9ee35..6cf835f521 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -4,8 +4,10 @@ import ( "context" "net/url" "testing" + "time" - "github.com/crewjam/saml" + crewjam_saml "github.com/crewjam/saml" + goldap "github.com/go-ldap/ldap/v3" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,6 +28,7 @@ import ( "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" rep_idp "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/repository/instance" @@ -740,6 +743,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { false, gu.Ptr(domain.SAMLNameIDFormatUnspecified), "", + false, rep_idp.Options{}, )), ), @@ -760,6 +764,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { false, gu.Ptr(domain.SAMLNameIDFormatUnspecified), "", + false, rep_idp.Options{}, )), ), @@ -867,7 +872,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "ro", 0), }, res{ err: zerrors.ThrowInternal(nil, "id", "encryption failed"), @@ -888,7 +893,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "ro", 0), idpSession: &oauth.Session{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ @@ -922,6 +927,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { Crypted: []byte("accessToken"), }, "idToken", + time.Time{}, ) return event }(), @@ -930,7 +936,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), idpSession: &openid.Session{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ @@ -973,7 +979,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { ctx context.Context writeModel *IDPIntentWriteModel idpUser idp.User - assertion *saml.Assertion + session *saml.Session userID string } type res struct { @@ -998,7 +1004,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "ro", 0), }, res{ err: zerrors.ThrowInternal(nil, "id", "encryption failed"), @@ -1023,14 +1029,17 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { KeyID: "id", Crypted: []byte(""), }, + time.Time{}, ), ), ), }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), - assertion: &saml.Assertion{ID: "id"}, + writeModel: NewIDPIntentWriteModel("id", "instance", 0), + session: &saml.Session{ + Assertion: &crewjam_saml.Assertion{ID: "id"}, + }, idpUser: openid.NewUser(&oidc.UserInfo{ Subject: "id", UserInfoProfile: oidc.UserInfoProfile{ @@ -1061,14 +1070,17 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { KeyID: "id", Crypted: []byte(""), }, + time.Time{}, ), ), ), }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), - assertion: &saml.Assertion{ID: "id"}, + writeModel: NewIDPIntentWriteModel("id", "instance", 0), + session: &saml.Session{ + Assertion: &crewjam_saml.Assertion{ID: "id"}, + }, idpUser: openid.NewUser(&oidc.UserInfo{ Subject: "id", UserInfoProfile: oidc.UserInfoProfile{ @@ -1088,7 +1100,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.idpConfigEncryption, } - got, err := c.SucceedSAMLIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.assertion) + got, err := c.SucceedSAMLIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.session) require.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.token, got) }) @@ -1128,7 +1140,7 @@ func TestCommands_RequestSAMLIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), request: "request", }, res{}, @@ -1156,7 +1168,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { writeModel *IDPIntentWriteModel idpUser idp.User userID string - attributes map[string][]string + session *ldap.Session } type res struct { token string @@ -1180,7 +1192,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), }, res{ err: zerrors.ThrowInternal(nil, "id", "encryption failed"), @@ -1200,14 +1212,24 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { "username", "", map[string][]string{"id": {"id"}}, + time.Time{}, ), ), ), }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), - attributes: map[string][]string{"id": {"id"}}, + writeModel: NewIDPIntentWriteModel("id", "instance", 0), + session: &ldap.Session{ + Entry: &goldap.Entry{ + Attributes: []*goldap.EntryAttribute{ + { + Name: "id", + Values: []string{"id"}, + }, + }, + }, + }, idpUser: ldap.NewUser( "id", "", @@ -1235,7 +1257,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.idpConfigEncryption, } - got, err := c.SucceedLDAPIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.attributes) + got, err := c.SucceedLDAPIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.session) require.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.token, got) }) @@ -1275,7 +1297,7 @@ func TestCommands_FailIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), reason: "reason", }, res{ diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index 5257d38bf4..188d45e6ec 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -1760,6 +1760,7 @@ type SAMLIDPWriteModel struct { WithSignedRequest bool NameIDFormat *domain.SAMLNameIDFormat TransientMappingAttributeName string + FederatedLogoutEnabled bool idp.Options State domain.IDPState @@ -1788,6 +1789,7 @@ func (wm *SAMLIDPWriteModel) reduceAddedEvent(e *idp.SAMLIDPAddedEvent) { wm.WithSignedRequest = e.WithSignedRequest wm.NameIDFormat = e.NameIDFormat wm.TransientMappingAttributeName = e.TransientMappingAttributeName + wm.FederatedLogoutEnabled = e.FederatedLogoutEnabled wm.Options = e.Options wm.State = domain.IDPStateActive } @@ -1817,6 +1819,9 @@ func (wm *SAMLIDPWriteModel) reduceChangedEvent(e *idp.SAMLIDPChangedEvent) { if e.TransientMappingAttributeName != nil { wm.TransientMappingAttributeName = *e.TransientMappingAttributeName } + if e.FederatedLogoutEnabled != nil { + wm.FederatedLogoutEnabled = *e.FederatedLogoutEnabled + } wm.Options.ReduceChanges(e.OptionChanges) } @@ -1830,6 +1835,7 @@ func (wm *SAMLIDPWriteModel) NewChanges( withSignedRequest bool, nameIDFormat *domain.SAMLNameIDFormat, transientMappingAttributeName string, + federatedLogoutEnabled bool, options idp.Options, ) ([]idp.SAMLIDPChanges, error) { changes := make([]idp.SAMLIDPChanges, 0) @@ -1861,6 +1867,9 @@ func (wm *SAMLIDPWriteModel) NewChanges( if wm.TransientMappingAttributeName != transientMappingAttributeName { changes = append(changes, idp.ChangeSAMLTransientMappingAttributeName(transientMappingAttributeName)) } + if wm.FederatedLogoutEnabled != federatedLogoutEnabled { + changes = append(changes, idp.ChangeSAMLFederatedLogoutEnabled(federatedLogoutEnabled)) + } opts := wm.Options.Changes(options) if !opts.IsZero() { changes = append(changes, idp.ChangeSAMLOptions(opts)) @@ -1899,6 +1908,7 @@ func (wm *SAMLIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encryp if wm.TransientMappingAttributeName != "" { opts = append(opts, saml2.WithTransientMappingAttributeName(wm.TransientMappingAttributeName)) } + // TODO: ? if wm.FederatedLogoutEnabled opts = append(opts, saml2.WithCustomRequestTracker( requesttracker.New( addRequest, diff --git a/internal/command/instance.go b/internal/command/instance.go index 1080168842..cfafb1d298 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -2,6 +2,7 @@ package command import ( "context" + "strings" "time" "github.com/zitadel/logging" @@ -220,7 +221,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str if err := setup.generateIDs(c.idGenerator); err != nil { return "", "", nil, nil, err } - ctx = contextWithInstanceSetupInfo(ctx, setup.zitadel.instanceID, setup.zitadel.projectID, setup.zitadel.consoleAppID, c.externalDomain) + ctx = contextWithInstanceSetupInfo(ctx, setup.zitadel.instanceID, setup.zitadel.projectID, setup.zitadel.consoleAppID, c.externalDomain, setup.DefaultLanguage) validations, pat, machineKey, err := setUpInstance(ctx, c, setup) if err != nil { @@ -254,19 +255,22 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str return setup.zitadel.instanceID, token, machineKey, details, nil } -func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string) context.Context { - return authz.WithConsole( - authz.SetCtxData( - http.WithRequestedHost( - authz.WithInstanceID( - ctx, - instanceID), - externalDomain, +func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string, defaultLanguage language.Tag) context.Context { + return authz.WithDefaultLanguage( + authz.WithConsole( + authz.SetCtxData( + http.WithRequestedHost( + authz.WithInstanceID( + ctx, + instanceID), + externalDomain, + ), + authz.CtxData{ResourceOwner: instanceID}, ), - authz.CtxData{ResourceOwner: instanceID}, + projectID, + consoleAppID, ), - projectID, - consoleAppID, + defaultLanguage, ) } @@ -666,7 +670,7 @@ func setupMessageTexts(validations *[]preparation.Validation, setupMessageTexts func (c *Commands) UpdateInstance(ctx context.Context, name string) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - validation := c.prepareUpdateInstance(instanceAgg, name) + validation := c.prepareUpdateInstance(instanceAgg, strings.TrimSpace(name)) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err @@ -885,7 +889,12 @@ func getSystemConfigWriteModel(ctx context.Context, filter preparation.FilterToQ } func (c *Commands) RemoveInstance(ctx context.Context, id string) (*domain.ObjectDetails, error) { - instanceAgg := instance.NewAggregate(id) + instID := strings.TrimSpace(id) + if instID == "" || len(instID) > 200 { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-VeS2zI", "Errors.Invalid.Argument") + } + + instanceAgg := instance.NewAggregate(instID) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareRemoveInstance(instanceAgg)) if err != nil { return nil, err diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go index 348f55cd9c..90efb3edd4 100644 --- a/internal/command/instance_idp.go +++ b/internal/command/instance_idp.go @@ -1795,6 +1795,7 @@ func (c *Commands) prepareAddInstanceSAMLProvider(a *instance.Aggregate, writeMo provider.WithSignedRequest, provider.NameIDFormat, provider.TransientMappingAttributeName, + provider.FederatedLogoutEnabled, provider.IDPOptions, ), }, nil @@ -1848,6 +1849,7 @@ func (c *Commands) prepareUpdateInstanceSAMLProvider(a *instance.Aggregate, writ provider.WithSignedRequest, provider.NameIDFormat, provider.TransientMappingAttributeName, + provider.FederatedLogoutEnabled, provider.IDPOptions, ) if err != nil || event == nil { @@ -1893,6 +1895,7 @@ func (c *Commands) prepareRegenerateInstanceSAMLProviderCertificate(a *instance. writeModel.WithSignedRequest, writeModel.NameIDFormat, writeModel.TransientMappingAttributeName, + writeModel.FederatedLogoutEnabled, writeModel.Options, ) if err != nil || event == nil { diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go index d94c19d318..03d9cd9c36 100644 --- a/internal/command/instance_idp_model.go +++ b/internal/command/instance_idp_model.go @@ -923,6 +923,7 @@ func (wm *InstanceSAMLIDPWriteModel) NewChangedEvent( withSignedRequest bool, nameIDFormat *domain.SAMLNameIDFormat, transientMappingAttributeName string, + federatedLogoutEnabled bool, options idp.Options, ) (*instance.SAMLIDPChangedEvent, error) { changes, err := wm.SAMLIDPWriteModel.NewChanges( @@ -935,6 +936,7 @@ func (wm *InstanceSAMLIDPWriteModel) NewChangedEvent( withSignedRequest, nameIDFormat, transientMappingAttributeName, + federatedLogoutEnabled, options, ) if err != nil || len(changes) == 0 { diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index 7002598af5..4805d075dd 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -5437,6 +5437,7 @@ func TestCommandSide_AddInstanceSAMLIDP(t *testing.T) { false, nil, "", + false, idp.Options{}, ), ), @@ -5478,6 +5479,7 @@ func TestCommandSide_AddInstanceSAMLIDP(t *testing.T) { true, gu.Ptr(domain.SAMLNameIDFormatTransient), "customAttribute", + true, idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -5500,6 +5502,7 @@ func TestCommandSide_AddInstanceSAMLIDP(t *testing.T) { WithSignedRequest: true, NameIDFormat: gu.Ptr(domain.SAMLNameIDFormatTransient), TransientMappingAttributeName: "customAttribute", + FederatedLogoutEnabled: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -5665,6 +5668,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) { false, nil, "", + false, idp.Options{}, )), ), @@ -5703,6 +5707,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) { false, gu.Ptr(domain.SAMLNameIDFormatUnspecified), "", + false, idp.Options{}, )), ), @@ -5718,6 +5723,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) { idp.ChangeSAMLWithSignedRequest(true), idp.ChangeSAMLNameIDFormat(gu.Ptr(domain.SAMLNameIDFormatTransient)), idp.ChangeSAMLTransientMappingAttributeName("customAttribute"), + idp.ChangeSAMLFederatedLogoutEnabled(true), idp.ChangeSAMLOptions(idp.OptionChanges{ IsCreationAllowed: &t, IsLinkingAllowed: &t, @@ -5742,6 +5748,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) { WithSignedRequest: true, NameIDFormat: gu.Ptr(domain.SAMLNameIDFormatTransient), TransientMappingAttributeName: "customAttribute", + FederatedLogoutEnabled: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -5845,6 +5852,7 @@ func TestCommandSide_RegenerateInstanceSAMLProviderCertificate(t *testing.T) { false, gu.Ptr(domain.SAMLNameIDFormatUnspecified), "", + false, idp.Options{}, )), ), diff --git a/internal/command/instance_member.go b/internal/command/instance_member.go index ee9bf15f84..a33635e8f5 100644 --- a/internal/command/instance_member.go +++ b/internal/command/instance_member.go @@ -22,7 +22,7 @@ func (c *Commands) AddInstanceMemberCommand(a *instance.Aggregate, userID string return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-4m0fS", "Errors.IAM.MemberInvalid") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, userID, "", false); err != nil || !exists { return nil, zerrors.ThrowPreconditionFailed(err, "INSTA-GSXOn", "Errors.User.NotFound") } if isMember, err := IsInstanceMember(ctx, filter, a.ID, userID); err != nil || isMember { diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go index 2ea248dfb1..2b82818a7e 100644 --- a/internal/command/instance_test.go +++ b/internal/command/instance_test.go @@ -345,6 +345,7 @@ func instanceElementsEvents(ctx context.Context, instanceID, instanceName string instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeOTPEmail, 8, 5*time.Minute, false, false, true, false), } } + func instanceElementsConfig() *SecretGenerators { return &SecretGenerators{ ClientSecret: &crypto.GeneratorConfig{Length: 64, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true}, @@ -668,22 +669,23 @@ func TestCommandSide_setupMinimalInterfaces(t *testing.T) { eventstore: expectEventstore( slices.Concat( projectFilters(), - []expect{expectPush( - projectAddedEvents(context.Background(), - "INSTANCE", - "ORG", - "PROJECT", - "owner", - false, - )..., - ), + []expect{ + expectPush( + projectAddedEvents(context.Background(), + "INSTANCE", + "ORG", + "PROJECT", + "owner", + false, + )..., + ), }, )..., ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, projectClientIDs()...), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgAgg: org.NewAggregate("ORG"), owner: "owner", @@ -767,7 +769,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { }, }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgAgg: org.NewAggregate("ORG"), human: instanceSetupHumanConfig(), @@ -806,7 +808,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgAgg: org.NewAggregate("ORG"), machine: instanceSetupMachineConfig(), @@ -855,7 +857,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgAgg: org.NewAggregate("ORG"), machine: instanceSetupMachineConfig(), @@ -972,7 +974,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgName: "ZITADEL", machine: &AddMachine{ @@ -1097,7 +1099,7 @@ func TestCommandSide_setupInstanceElements(t *testing.T) { ), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), setup: setupInstanceElementsConfig(), }, @@ -1183,7 +1185,7 @@ func TestCommandSide_setUpInstance(t *testing.T) { }, }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), setup: setupInstanceConfig(), }, res: res{ @@ -1257,7 +1259,7 @@ func TestCommandSide_UpdateInstance(t *testing.T) { }, args: args{ ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - name: "", + name: " ", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -1404,6 +1406,32 @@ func TestCommandSide_RemoveInstance(t *testing.T) { args args res res }{ + { + name: "instance empty, invalid argument error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), " "), + instanceID: " ", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "instance too long, invalid argument error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "averylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonginstance"), + instanceID: "averylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonginstance", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, { name: "instance not existing, not found error", fields: fields{ diff --git a/internal/command/instance_trusted_domain.go b/internal/command/instance_trusted_domain.go index f404e6665a..7d3e6abeb1 100644 --- a/internal/command/instance_trusted_domain.go +++ b/internal/command/instance_trusted_domain.go @@ -34,6 +34,14 @@ func (c *Commands) AddTrustedDomain(ctx context.Context, trustedDomain string) ( } func (c *Commands) RemoveTrustedDomain(ctx context.Context, trustedDomain string) (*domain.ObjectDetails, error) { + trustedDomain = strings.TrimSpace(trustedDomain) + if trustedDomain == "" || len(trustedDomain) > 253 { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-ajAzwu", "Errors.Invalid.Argument") + } + if !allowDomainRunes.MatchString(trustedDomain) { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-lfs3Te", "Errors.Instance.Domain.InvalidCharacter") + } + model := NewInstanceTrustedDomainsWriteModel(ctx) err := c.eventstore.FilterToQueryReducer(ctx, model) if err != nil { diff --git a/internal/command/instance_trusted_domain_test.go b/internal/command/instance_trusted_domain_test.go index 3caef90f01..4ba9f773ed 100644 --- a/internal/command/instance_trusted_domain_test.go +++ b/internal/command/instance_trusted_domain_test.go @@ -142,6 +142,45 @@ func TestCommands_RemoveTrustedDomain(t *testing.T) { args args want want }{ + { + name: "domain empty string, error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: " ", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-ajAzwu", "Errors.Invalid.Argument"), + }, + }, + { + name: "domain invalid character, error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "? ", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-lfs3Te", "Errors.Instance.Domain.InvalidCharacter"), + }, + }, + { + name: "domain length exceeded, error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "averylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongdomain", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-ajAzwu", "Errors.Invalid.Argument"), + }, + }, { name: "domain does not exists, error", fields: fields{ diff --git a/internal/command/org.go b/internal/command/org.go index a018a90c82..faab882d68 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -31,6 +31,7 @@ type OrgSetup struct { Name string CustomDomain string Admins []*OrgSetupAdmin + OrgID string } // OrgSetupAdmin describes a user to be created (Human / Machine) or an existing (ID) to be used for an org setup. @@ -53,7 +54,11 @@ type orgSetupCommands struct { type CreatedOrg struct { ObjectDetails *domain.ObjectDetails - CreatedAdmins []*CreatedOrgAdmin + OrgAdmins []OrgAdmin +} + +type OrgAdmin interface { + GetID() string } type CreatedOrgAdmin struct { @@ -64,6 +69,25 @@ type CreatedOrgAdmin struct { MachineKey *MachineKey } +func (a *CreatedOrgAdmin) GetID() string { + return a.ID +} + +type AssignedOrgAdmin struct { + ID string +} + +func (a *AssignedOrgAdmin) GetID() string { + return a.ID +} + +func (o *OrgSetup) Validate() (err error) { + if o.OrgID != "" && strings.TrimSpace(o.OrgID) == "" { + return zerrors.ThrowInvalidArgument(nil, "ORG-4ABd3", "Errors.Invalid.Argument") + } + return nil +} + func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID string, allowInitialMail bool, userIDs ...string) (_ *CreatedOrg, err error) { cmds := c.newOrgSetupCommands(ctx, orgID, o) for _, admin := range o.Admins { @@ -180,14 +204,15 @@ func (c *orgSetupCommands) push(ctx context.Context) (_ *CreatedOrg, err error) EventDate: events[len(events)-1].CreatedAt(), ResourceOwner: c.aggregate.ID, }, - CreatedAdmins: c.createdAdmins(), + OrgAdmins: c.createdAdmins(), }, nil } -func (c *orgSetupCommands) createdAdmins() []*CreatedOrgAdmin { - users := make([]*CreatedOrgAdmin, 0, len(c.admins)) +func (c *orgSetupCommands) createdAdmins() []OrgAdmin { + users := make([]OrgAdmin, 0, len(c.admins)) for _, admin := range c.admins { if admin.ID != "" && admin.Human == nil { + users = append(users, &AssignedOrgAdmin{ID: admin.ID}) continue } if admin.Human != nil { @@ -233,12 +258,19 @@ func (c *orgSetupCommands) createdMachineAdmin(admin *OrgSetupAdmin) *CreatedOrg } func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, allowInitialMail bool, userIDs ...string) (*CreatedOrg, error) { - orgID, err := c.idGenerator.Next() - if err != nil { + if err := o.Validate(); err != nil { return nil, err } - return c.setUpOrgWithIDs(ctx, o, orgID, allowInitialMail, userIDs...) + if o.OrgID == "" { + var err error + o.OrgID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + + return c.setUpOrgWithIDs(ctx, o, o.OrgID, allowInitialMail, userIDs...) } // AddOrgCommand defines the commands to create a new org, @@ -285,7 +317,7 @@ func (c *Commands) checkOrgExists(ctx context.Context, orgID string) error { return nil } -func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner, orgID string, claimedUserIDs []string) (_ *domain.Org, err error) { +func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner, orgID string, setOrgInactive bool, claimedUserIDs []string) (_ *domain.Org, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -297,7 +329,7 @@ func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner return nil, zerrors.ThrowNotFound(nil, "ORG-lapo2m", "Errors.Org.AlreadyExisting") } - return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, claimedUserIDs) + return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, setOrgInactive, claimedUserIDs) } func (c *Commands) AddOrg(ctx context.Context, name, userID, resourceOwner string, claimedUserIDs []string) (*domain.Org, error) { @@ -310,10 +342,10 @@ func (c *Commands) AddOrg(ctx context.Context, name, userID, resourceOwner strin return nil, zerrors.ThrowInternal(err, "COMMA-OwciI", "Errors.Internal") } - return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, claimedUserIDs) + return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, false, claimedUserIDs) } -func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, resourceOwner, orgID string, claimedUserIDs []string) (_ *domain.Org, err error) { +func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, resourceOwner, orgID string, setOrgInactive bool, claimedUserIDs []string) (_ *domain.Org, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -331,10 +363,15 @@ func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, reso return nil, err } events = append(events, orgMemberEvent) + if setOrgInactive { + deactivateOrgEvent := org.NewOrgDeactivatedEvent(ctx, orgAgg) + events = append(events, deactivateOrgEvent) + } pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, err } + err = AppendAndReduce(addedOrg, pushedEvents...) if err != nil { return nil, err @@ -453,7 +490,7 @@ func (c *Commands) prepareRemoveOrg(a *org.Aggregate) preparation.Validation { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-wG9p1", "Errors.Org.DefaultOrgNotDeletable") } - err := c.checkProjectExists(ctx, instance.ProjectID(), a.ID) + _, err := c.checkProjectExists(ctx, instance.ProjectID(), a.ID) // if there is no error, the ZITADEL project was found on the org to be deleted if err == nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMA-AF3JW", "Errors.Org.ZitadelOrgNotDeletable") diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go index b72fc1fd77..6cae78acc7 100644 --- a/internal/command/org_idp.go +++ b/internal/command/org_idp.go @@ -1768,6 +1768,7 @@ func (c *Commands) prepareAddOrgSAMLProvider(a *org.Aggregate, writeModel *OrgSA provider.WithSignedRequest, provider.NameIDFormat, provider.TransientMappingAttributeName, + provider.FederatedLogoutEnabled, provider.IDPOptions, ), }, nil @@ -1821,6 +1822,7 @@ func (c *Commands) prepareUpdateOrgSAMLProvider(a *org.Aggregate, writeModel *Or provider.WithSignedRequest, provider.NameIDFormat, provider.TransientMappingAttributeName, + provider.FederatedLogoutEnabled, provider.IDPOptions, ) if err != nil || event == nil { @@ -1866,6 +1868,7 @@ func (c *Commands) prepareRegenerateOrgSAMLProviderCertificate(a *org.Aggregate, writeModel.WithSignedRequest, writeModel.NameIDFormat, writeModel.TransientMappingAttributeName, + writeModel.FederatedLogoutEnabled, writeModel.Options, ) if err != nil || event == nil { diff --git a/internal/command/org_idp_model.go b/internal/command/org_idp_model.go index 3baea11495..fdafb7f087 100644 --- a/internal/command/org_idp_model.go +++ b/internal/command/org_idp_model.go @@ -935,6 +935,7 @@ func (wm *OrgSAMLIDPWriteModel) NewChangedEvent( withSignedRequest bool, nameIDFormat *domain.SAMLNameIDFormat, transientMappingAttributeName string, + federatedLogoutEnabled bool, options idp.Options, ) (*org.SAMLIDPChangedEvent, error) { changes, err := wm.SAMLIDPWriteModel.NewChanges( @@ -947,6 +948,7 @@ func (wm *OrgSAMLIDPWriteModel) NewChangedEvent( withSignedRequest, nameIDFormat, transientMappingAttributeName, + federatedLogoutEnabled, options, ) if err != nil || len(changes) == 0 { diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index 9959ced97d..54f508da30 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -5519,6 +5519,7 @@ func TestCommandSide_AddOrgSAMLIDP(t *testing.T) { false, nil, "", + false, idp.Options{}, ), ), @@ -5560,6 +5561,7 @@ func TestCommandSide_AddOrgSAMLIDP(t *testing.T) { true, gu.Ptr(domain.SAMLNameIDFormatTransient), "customAttribute", + true, idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -5583,6 +5585,7 @@ func TestCommandSide_AddOrgSAMLIDP(t *testing.T) { WithSignedRequest: true, NameIDFormat: gu.Ptr(domain.SAMLNameIDFormatTransient), TransientMappingAttributeName: "customAttribute", + FederatedLogoutEnabled: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -5756,6 +5759,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) { false, nil, "", + false, idp.Options{}, )), ), @@ -5795,6 +5799,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) { false, gu.Ptr(domain.SAMLNameIDFormatUnspecified), "", + false, idp.Options{}, )), ), @@ -5810,6 +5815,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) { idp.ChangeSAMLWithSignedRequest(true), idp.ChangeSAMLNameIDFormat(gu.Ptr(domain.SAMLNameIDFormatTransient)), idp.ChangeSAMLTransientMappingAttributeName("customAttribute"), + idp.ChangeSAMLFederatedLogoutEnabled(true), idp.ChangeSAMLOptions(idp.OptionChanges{ IsCreationAllowed: &t, IsLinkingAllowed: &t, @@ -5835,6 +5841,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) { WithSignedRequest: true, NameIDFormat: gu.Ptr(domain.SAMLNameIDFormatTransient), TransientMappingAttributeName: "customAttribute", + FederatedLogoutEnabled: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -5943,6 +5950,7 @@ func TestCommandSide_RegenerateOrgSAMLProviderCertificate(t *testing.T) { false, gu.Ptr(domain.SAMLNameIDFormatUnspecified), "", + false, idp.Options{}, )), ), diff --git a/internal/command/org_member.go b/internal/command/org_member.go index ae9bef2151..bf1ae91d8a 100644 --- a/internal/command/org_member.go +++ b/internal/command/org_member.go @@ -28,7 +28,7 @@ func (c *Commands) AddOrgMemberCommand(a *org.Aggregate, userID string, roles .. ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, userID, "", false); err != nil || !exists { return nil, zerrors.ThrowPreconditionFailed(err, "ORG-GoXOn", "Errors.User.NotFound") } if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember { diff --git a/internal/command/org_test.go b/internal/command/org_test.go index 4ec85d61e1..4239be760a 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -1344,6 +1344,22 @@ func TestCommandSide_SetUpOrg(t *testing.T) { err: zerrors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument"), }, }, + { + name: "org id empty, error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), + setupOrg: &OrgSetup{ + Name: "Org", + OrgID: " ", + }, + }, + res: res{ + err: zerrors.ThrowInvalidArgument(nil, "ORG-4ABd3", "Errors.Invalid.Argument"), + }, + }, { name: "userID not existing, error", fields: fields{ @@ -1515,14 +1531,53 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "orgID", }, - CreatedAdmins: []*CreatedOrgAdmin{ - { + OrgAdmins: []OrgAdmin{ + &CreatedOrgAdmin{ ID: "userID", }, }, }, }, }, + { + name: "no human added, custom org ID", + fields: fields{ + eventstore: expectEventstore( + expectPush( + eventFromEventPusher(org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "Org", + )), + eventFromEventPusher(org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, "org.iam-domain", + )), + eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "org.iam-domain", + )), + eventFromEventPusher(org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "org.iam-domain", + )), + ), + ), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), + setupOrg: &OrgSetup{ + Name: "Org", + OrgID: "custom-org-ID", + }, + }, + res: res{ + createdOrg: &CreatedOrg{ + ObjectDetails: &domain.ObjectDetails{ + ResourceOwner: "custom-org-ID", + }, + OrgAdmins: []OrgAdmin{}, + }, + }, + }, { name: "existing human added", fields: fields{ @@ -1586,7 +1641,11 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "orgID", }, - CreatedAdmins: []*CreatedOrgAdmin{}, + OrgAdmins: []OrgAdmin{ + &AssignedOrgAdmin{ + ID: "userID", + }, + }, }, }, }, @@ -1696,8 +1755,8 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "orgID", }, - CreatedAdmins: []*CreatedOrgAdmin{ - { + OrgAdmins: []OrgAdmin{ + &CreatedOrgAdmin{ ID: "userID", PAT: &PersonalAccessToken{ ObjectRoot: models.ObjectRoot{ diff --git a/internal/command/permission_checks.go b/internal/command/permission_checks.go new file mode 100644 index 0000000000..6bfeaae219 --- /dev/null +++ b/internal/command/permission_checks.go @@ -0,0 +1,87 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/project" + "github.com/zitadel/zitadel/internal/v2/user" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type PermissionCheck func(resourceOwner, aggregateID string) error + +func (c *Commands) newPermissionCheck(ctx context.Context, permission string, aggregateType eventstore.AggregateType) PermissionCheck { + return func(resourceOwner, aggregateID string) error { + if aggregateID == "" { + return zerrors.ThrowInternal(nil, "COMMAND-ulBlS", "Errors.IDMissing") + } + // For example if a write model didn't query any events, the resource owner is probably empty. + // In this case, we have to query an event on the given aggregate to get the resource owner. + if resourceOwner == "" { + r := NewResourceOwnerModel(authz.GetInstance(ctx).InstanceID(), aggregateType, aggregateID) + err := c.eventstore.FilterToQueryReducer(ctx, r) + if err != nil { + return err + } + resourceOwner = r.resourceOwner + } + if resourceOwner == "" { + return zerrors.ThrowNotFound(nil, "COMMAND-4g3xq", "Errors.NotFound") + } + return c.checkPermission(ctx, permission, resourceOwner, aggregateID) + } +} + +func (c *Commands) checkPermissionOnUser(ctx context.Context, permission string) PermissionCheck { + return func(resourceOwner, aggregateID string) error { + if aggregateID != "" && aggregateID == authz.GetCtxData(ctx).UserID { + return nil + } + return c.newPermissionCheck(ctx, permission, user.AggregateType)(resourceOwner, aggregateID) + } +} + +func (c *Commands) NewPermissionCheckUserWrite(ctx context.Context) PermissionCheck { + return c.checkPermissionOnUser(ctx, domain.PermissionUserWrite) +} + +func (c *Commands) checkPermissionDeleteUser(ctx context.Context, resourceOwner, userID string) error { + return c.checkPermissionOnUser(ctx, domain.PermissionUserDelete)(resourceOwner, userID) +} + +func (c *Commands) checkPermissionUpdateUser(ctx context.Context, resourceOwner, userID string) error { + return c.NewPermissionCheckUserWrite(ctx)(resourceOwner, userID) +} + +func (c *Commands) checkPermissionUpdateUserCredentials(ctx context.Context, resourceOwner, userID string) error { + return c.checkPermissionOnUser(ctx, domain.PermissionUserCredentialWrite)(resourceOwner, userID) +} + +func (c *Commands) checkPermissionDeleteProject(ctx context.Context, resourceOwner, projectID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectDelete, project.AggregateType)(resourceOwner, projectID) +} + +func (c *Commands) checkPermissionUpdateProject(ctx context.Context, resourceOwner, projectID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectWrite, project.AggregateType)(resourceOwner, projectID) +} + +func (c *Commands) checkPermissionUpdateProjectGrant(ctx context.Context, resourceOwner, projectID, projectGrantID string) (err error) { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectID); err != nil { + return err + } + } + return nil +} + +func (c *Commands) checkPermissionDeleteProjectGrant(ctx context.Context, resourceOwner, projectID, projectGrantID string) (err error) { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantDelete, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantDelete, project.AggregateType)(resourceOwner, projectID); err != nil { + return err + } + } + return nil +} diff --git a/internal/command/permission_checks_test.go b/internal/command/permission_checks_test.go new file mode 100644 index 0000000000..5c36dc14f9 --- /dev/null +++ b/internal/command/permission_checks_test.go @@ -0,0 +1,278 @@ +package command + +import ( + "context" + "database/sql" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "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/repository" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommands_CheckPermission(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + domainPermissionCheck func(*testing.T) domain.PermissionCheck + } + type args struct { + ctx context.Context + permission string + aggregateType eventstore.AggregateType + resourceOwner, aggregateID string + } + type want struct { + err func(error) bool + } + ctx := context.Background() + filterErr := errors.New("filter error") + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "resource owner is given, no query", + fields: fields{ + domainPermissionCheck: mockDomainPermissionCheck( + ctx, + "permission", + "resourceOwner", + "aggregateID"), + }, + args: args{ + ctx: ctx, + permission: "permission", + resourceOwner: "resourceOwner", + aggregateID: "aggregateID", + }, + }, + { + name: "resource owner is empty, query for resource owner", + fields: fields{ + eventstore: expectEventstore( + expectFilter(&repository.Event{ + AggregateID: "aggregateID", + ResourceOwner: sql.NullString{String: "resourceOwner"}, + }), + ), + domainPermissionCheck: mockDomainPermissionCheck(ctx, "permission", "resourceOwner", "aggregateID"), + }, + args: args{ + ctx: ctx, + permission: "permission", + resourceOwner: "", + aggregateID: "aggregateID", + }, + }, + { + name: "resource owner is empty, query for resource owner, error", + fields: fields{ + eventstore: expectEventstore( + expectFilterError(filterErr), + ), + }, + args: args{ + ctx: ctx, + permission: "permission", + resourceOwner: "", + aggregateID: "aggregateID", + }, + want: want{ + err: func(err error) bool { + return errors.Is(err, filterErr) + }, + }, + }, + { + name: "resource owner is empty, query for resource owner, no events", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{ + ctx: ctx, + permission: "permission", + resourceOwner: "", + aggregateID: "aggregateID", + }, + want: want{ + err: zerrors.IsNotFound, + }, + }, + { + name: "no aggregateID, internal error", + args: args{ + ctx: ctx, + }, + want: want{ + err: zerrors.IsInternal, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + checkPermission: func(ctx context.Context, permission, orgID, resourceID string) (err error) { + assert.Failf(t, "Domain permission check should not be called", "Called c.checkPermission(%v,%v,%v,%v)", ctx, permission, orgID, resourceID) + return nil + }, + eventstore: expectEventstore()(t), + } + if tt.fields.domainPermissionCheck != nil { + c.checkPermission = tt.fields.domainPermissionCheck(t) + } + if tt.fields.eventstore != nil { + c.eventstore = tt.fields.eventstore(t) + } + err := c.newPermissionCheck(tt.args.ctx, tt.args.permission, tt.args.aggregateType)(tt.args.resourceOwner, tt.args.aggregateID) + if tt.want.err != nil { + assert.True(t, tt.want.err(err)) + } + }) + } +} + +func TestCommands_CheckPermissionUserWrite(t *testing.T) { + type fields struct { + domainPermissionCheck func(*testing.T) domain.PermissionCheck + } + type args struct { + ctx context.Context + resourceOwner, aggregateID string + } + type want struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "self, no permission check", + args: args{ + ctx: authz.SetCtxData(context.Background(), authz.CtxData{ + UserID: "aggregateID", + }), + resourceOwner: "resourceOwner", + aggregateID: "aggregateID", + }, + }, + { + name: "not self, permission check", + fields: fields{ + domainPermissionCheck: mockDomainPermissionCheck( + context.Background(), + "user.write", + "resourceOwner", + "foreignAggregateID"), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "resourceOwner", + aggregateID: "foreignAggregateID", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + checkPermission: func(ctx context.Context, permission, orgID, resourceID string) (err error) { + assert.Failf(t, "Domain permission check should not be called", "Called c.checkPermission(%v,%v,%v,%v)", ctx, permission, orgID, resourceID) + return nil + }, + } + if tt.fields.domainPermissionCheck != nil { + c.checkPermission = tt.fields.domainPermissionCheck(t) + } + err := c.NewPermissionCheckUserWrite(tt.args.ctx)(tt.args.resourceOwner, tt.args.aggregateID) + if tt.want.err != nil { + assert.True(t, tt.want.err(err)) + } + }) + } +} + +func TestCommands_CheckPermissionUserDelete(t *testing.T) { + type fields struct { + domainPermissionCheck func(*testing.T) domain.PermissionCheck + } + type args struct { + ctx context.Context + resourceOwner, aggregateID string + } + type want struct { + err func(error) bool + } + userCtx := authz.SetCtxData(context.Background(), authz.CtxData{ + UserID: "aggregateID", + }) + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "self, no permission check", + args: args{ + ctx: userCtx, + resourceOwner: "resourceOwner", + aggregateID: "aggregateID", + }, + }, + { + name: "not self, permission check", + fields: fields{ + domainPermissionCheck: mockDomainPermissionCheck( + context.Background(), + "user.delete", + "resourceOwner", + "foreignAggregateID"), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "resourceOwner", + aggregateID: "foreignAggregateID", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + checkPermission: func(ctx context.Context, permission, orgID, resourceID string) (err error) { + assert.Failf(t, "Domain permission check should not be called", "Called c.checkPermission(%v,%v,%v,%v)", ctx, permission, orgID, resourceID) + return nil + }, + } + if tt.fields.domainPermissionCheck != nil { + c.checkPermission = tt.fields.domainPermissionCheck(t) + } + err := c.checkPermissionDeleteUser(tt.args.ctx, tt.args.resourceOwner, tt.args.aggregateID) + if tt.want.err != nil { + assert.True(t, tt.want.err(err)) + } + }) + } +} + +func mockDomainPermissionCheck(expectCtx context.Context, expectPermission, expectResourceOwner, expectResourceID string) func(t *testing.T) domain.PermissionCheck { + return func(t *testing.T) domain.PermissionCheck { + return func(ctx context.Context, permission, orgID, resourceID string) (err error) { + assert.Equal(t, expectCtx, ctx) + assert.Equal(t, expectPermission, permission) + assert.Equal(t, expectResourceOwner, orgID) + assert.Equal(t, expectResourceID, resourceID) + return nil + } + } +} diff --git a/internal/command/project.go b/internal/command/project.go index df4f8ab545..40aa79f186 100644 --- a/internal/command/project.go +++ b/internal/command/project.go @@ -3,6 +3,7 @@ package command import ( "context" "strings" + "time" "github.com/zitadel/logging" @@ -10,6 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/project" @@ -17,67 +19,60 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectWithID(ctx context.Context, project *domain.Project, resourceOwner, projectID string) (_ *domain.Project, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - if resourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-w8tnSoJxtn", "Errors.ResourceOwnerMissing") - } - if projectID == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nDXf5vXoUj", "Errors.IDMissing") - } - if !project.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") - } - project, err = c.addProjectWithID(ctx, project, resourceOwner, projectID) - if err != nil { - return nil, err - } - return project, nil +type AddProject struct { + models.ObjectRoot + + Name string + ProjectRoleAssertion bool + ProjectRoleCheck bool + HasProjectCheck bool + PrivateLabelingSetting domain.PrivateLabelingSetting } -func (c *Commands) AddProject(ctx context.Context, project *domain.Project, resourceOwner string) (_ *domain.Project, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - if !project.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") +func (p *AddProject) IsValid() error { + if p.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-fmq7bqQX1s", "Errors.ResourceOwnerMissing") } - if resourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-fmq7bqQX1s", "Errors.ResourceOwnerMissing") + if p.Name == "" { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") } - - projectID, err := c.idGenerator.Next() - if err != nil { - return nil, err - } - - project, err = c.addProjectWithID(ctx, project, resourceOwner, projectID) - if err != nil { - return nil, err - } - return project, nil + return nil } -func (c *Commands) addProjectWithID(ctx context.Context, projectAdd *domain.Project, resourceOwner, projectID string) (_ *domain.Project, err error) { - projectAdd.AggregateID = projectID - projectWriteModel, err := c.getProjectWriteModelByID(ctx, projectAdd.AggregateID, resourceOwner) +func (c *Commands) AddProject(ctx context.Context, add *AddProject) (_ *domain.ObjectDetails, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if err := add.IsValid(); err != nil { + return nil, err + } + + if add.AggregateID == "" { + add.AggregateID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + wm, err := c.getProjectWriteModelByID(ctx, add.AggregateID, add.ResourceOwner) if err != nil { return nil, err } - if isProjectStateExists(projectWriteModel.State) { + if isProjectStateExists(wm.State) { return nil, zerrors.ThrowAlreadyExists(nil, "COMMAND-opamwu", "Errors.Project.AlreadyExisting") } + if err := c.checkPermissionUpdateProject(ctx, wm.ResourceOwner, wm.AggregateID); err != nil { + return nil, err + } events := []eventstore.Command{ project.NewProjectAddedEvent( ctx, - //nolint: contextcheck - ProjectAggregateFromWriteModel(&projectWriteModel.WriteModel), - projectAdd.Name, - projectAdd.ProjectRoleAssertion, - projectAdd.ProjectRoleCheck, - projectAdd.HasProjectCheck, - projectAdd.PrivateLabelingSetting), + ProjectAggregateFromWriteModelWithCTX(ctx, &wm.WriteModel), + add.Name, + add.ProjectRoleAssertion, + add.ProjectRoleCheck, + add.HasProjectCheck, + add.PrivateLabelingSetting), } postCommit, err := c.projectCreatedMilestone(ctx, &events) if err != nil { @@ -88,11 +83,11 @@ func (c *Commands) addProjectWithID(ctx context.Context, projectAdd *domain.Proj return nil, err } postCommit(ctx) - err = AppendAndReduce(projectWriteModel, pushedEvents...) + err = AppendAndReduce(wm, pushedEvents...) if err != nil { return nil, err } - return projectWriteModelToProject(projectWriteModel), nil + return writeModelToObjectDetails(&wm.WriteModel), nil } func AddProjectCommand( @@ -177,7 +172,7 @@ func (c *Commands) projectState(ctx context.Context, projectID, resourceOwner st ) } -func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOwner string) (err error) { +func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOwner string) (_ string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -185,50 +180,69 @@ func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOw return c.checkProjectExistsOld(ctx, projectID, resourceOwner) } - _, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) + agg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) if err != nil || !state.Valid() { - return zerrors.ThrowPreconditionFailed(err, "COMMA-VCnwD", "Errors.Project.NotFound") + return "", zerrors.ThrowPreconditionFailed(err, "COMMA-VCnwD", "Errors.Project.NotFound") + } + return agg.ResourceOwner, nil +} + +type ChangeProject struct { + models.ObjectRoot + + Name *string + ProjectRoleAssertion *bool + ProjectRoleCheck *bool + HasProjectCheck *bool + PrivateLabelingSetting *domain.PrivateLabelingSetting +} + +func (p *ChangeProject) IsValid() error { + if p.AggregateID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") + } + if p.Name != nil && *p.Name == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") } return nil } -func (c *Commands) ChangeProject(ctx context.Context, projectChange *domain.Project, resourceOwner string) (*domain.Project, error) { - if !projectChange.IsValid() || projectChange.AggregateID == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") +func (c *Commands) ChangeProject(ctx context.Context, change *ChangeProject) (_ *domain.ObjectDetails, err error) { + if err := change.IsValid(); err != nil { + return nil, err } - existingProject, err := c.getProjectWriteModelByID(ctx, projectChange.AggregateID, resourceOwner) + existing, err := c.getProjectWriteModelByID(ctx, change.AggregateID, change.ResourceOwner) if err != nil { return nil, err } - if !isProjectStateExists(existingProject.State) { + if !isProjectStateExists(existing.State) { return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound") } + if err := c.checkPermissionUpdateProject(ctx, existing.ResourceOwner, existing.AggregateID); err != nil { + return nil, err + } - projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) - changedEvent, hasChanged, err := existingProject.NewChangedEvent( + changedEvent := existing.NewChangedEvent( ctx, - projectAgg, - projectChange.Name, - projectChange.ProjectRoleAssertion, - projectChange.ProjectRoleCheck, - projectChange.HasProjectCheck, - projectChange.PrivateLabelingSetting) + ProjectAggregateFromWriteModelWithCTX(ctx, &existing.WriteModel), + change.Name, + change.ProjectRoleAssertion, + change.ProjectRoleCheck, + change.HasProjectCheck, + change.PrivateLabelingSetting) + if changedEvent == nil { + return writeModelToObjectDetails(&existing.WriteModel), nil + } + err = c.pushAppendAndReduce(ctx, existing, changedEvent) if err != nil { return nil, err } - if !hasChanged { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M0fs", "Errors.NoChangesFound") - } - err = c.pushAppendAndReduce(ctx, existingProject, changedEvent) - if err != nil { - return nil, err - } - return projectWriteModelToProject(existingProject), nil + return writeModelToObjectDetails(&existing.WriteModel), nil } func (c *Commands) DeactivateProject(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) { - if projectID == "" || resourceOwner == "" { + if projectID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-88iF0", "Errors.Project.ProjectIDMissing") } @@ -247,6 +261,9 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso if state != domain.ProjectStateActive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-mki55", "Errors.Project.NotActive") } + if err := c.checkPermissionUpdateProject(ctx, projectAgg.ResourceOwner, projectAgg.ID); err != nil { + return nil, err + } pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectDeactivatedEvent(ctx, projectAgg)) if err != nil { @@ -261,7 +278,7 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso } func (c *Commands) ReactivateProject(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) { - if projectID == "" || resourceOwner == "" { + if projectID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3ihsF", "Errors.Project.ProjectIDMissing") } @@ -280,6 +297,9 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso if state != domain.ProjectStateInactive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M9bs", "Errors.Project.NotInactive") } + if err := c.checkPermissionUpdateProject(ctx, projectAgg.ResourceOwner, projectAgg.ID); err != nil { + return nil, err + } pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectReactivatedEvent(ctx, projectAgg)) if err != nil { @@ -293,6 +313,7 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso }, nil } +// Deprecated: use commands.DeleteProject func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner string, cascadingUserGrantIDs ...string) (*domain.ObjectDetails, error) { if projectID == "" || resourceOwner == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-66hM9", "Errors.Project.ProjectIDMissing") @@ -316,15 +337,18 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s uniqueConstraints[i] = project.NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID.EntityID) } - projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) events := []eventstore.Command{ - project.NewProjectRemovedEvent(ctx, projectAgg, existingProject.Name, uniqueConstraints), + project.NewProjectRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingProject.WriteModel), + existingProject.Name, + uniqueConstraints, + ), } for _, grantID := range cascadingUserGrantIDs { event, _, err := c.removeUserGrant(ctx, grantID, "", true) if err != nil { - logging.LogWithFields("COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") continue } events = append(events, event) @@ -341,6 +365,53 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s return writeModelToObjectDetails(&existingProject.WriteModel), nil } +func (c *Commands) DeleteProject(ctx context.Context, id, resourceOwner string, cascadingUserGrantIDs ...string) (time.Time, error) { + if id == "" { + return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-obqos2l3no", "Errors.IDMissing") + } + + existing, err := c.getProjectWriteModelByID(ctx, id, resourceOwner) + if err != nil { + return time.Time{}, err + } + if !isProjectStateExists(existing.State) { + return existing.WriteModel.ChangeDate, nil + } + if err := c.checkPermissionDeleteProject(ctx, existing.ResourceOwner, existing.AggregateID); err != nil { + return time.Time{}, err + } + + samlEntityIDsAgg, err := c.getSAMLEntityIdsWriteModelByProjectID(ctx, id, resourceOwner) + if err != nil { + return time.Time{}, err + } + + uniqueConstraints := make([]*eventstore.UniqueConstraint, len(samlEntityIDsAgg.EntityIDs)) + for i, entityID := range samlEntityIDsAgg.EntityIDs { + uniqueConstraints[i] = project.NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID.EntityID) + } + events := []eventstore.Command{ + project.NewProjectRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existing.WriteModel), + existing.Name, + uniqueConstraints, + ), + } + for _, grantID := range cascadingUserGrantIDs { + event, _, err := c.removeUserGrant(ctx, grantID, "", true) + if err != nil { + logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + continue + } + events = append(events, event) + } + + if err := c.pushAppendAndReduce(ctx, existing, events...); err != nil { + return time.Time{}, err + } + return existing.WriteModel.ChangeDate, nil +} + func (c *Commands) getProjectWriteModelByID(ctx context.Context, projectID, resourceOwner string) (_ *ProjectWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/command/project_application_api.go b/internal/command/project_application_api.go index 21c3bc5ee7..5b8bbafcdf 100644 --- a/internal/command/project_application_api.go +++ b/internal/command/project_application_api.go @@ -79,7 +79,7 @@ func (c *Commands) AddAPIApplicationWithID(ctx context.Context, apiApp *domain.A return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-mabu12", "Errors.Project.App.AlreadyExisting") } - if err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil { + if _, err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil { return nil, err } return c.addAPIApplicationWithID(ctx, apiApp, resourceOwner, appID) @@ -90,7 +90,7 @@ func (c *Commands) AddAPIApplication(ctx context.Context, apiApp *domain.APIApp, return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-5m9E", "Errors.Project.App.Invalid") } - if err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil { + if _, err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil { return nil, err } if !apiApp.IsValid() { diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index fccb0efe06..491bd38fca 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -132,7 +132,7 @@ func (c *Commands) AddOIDCApplicationWithID(ctx context.Context, oidcApp *domain return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-lxowmp", "Errors.Project.App.AlreadyExisting") } - if err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil { + if _, err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil { return nil, err } return c.addOIDCApplicationWithID(ctx, oidcApp, resourceOwner, appID) @@ -142,7 +142,7 @@ func (c *Commands) AddOIDCApplication(ctx context.Context, oidcApp *domain.OIDCA if oidcApp == nil || oidcApp.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-34Fm0", "Errors.Project.App.Invalid") } - if err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil { + if _, err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil { return nil, err } if oidcApp.AppName == "" || !oidcApp.IsValid() { diff --git a/internal/command/project_application_saml.go b/internal/command/project_application_saml.go index b14bed0758..1a5cefa221 100644 --- a/internal/command/project_application_saml.go +++ b/internal/command/project_application_saml.go @@ -16,7 +16,7 @@ func (c *Commands) AddSAMLApplication(ctx context.Context, application *domain.S return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-35Fn0", "Errors.Project.App.Invalid") } - if err := c.checkProjectExists(ctx, application.AggregateID, resourceOwner); err != nil { + if _, err := c.checkProjectExists(ctx, application.AggregateID, resourceOwner); err != nil { return nil, err } addedApplication := NewSAMLApplicationWriteModel(application.AggregateID, resourceOwner) diff --git a/internal/command/project_grant.go b/internal/command/project_grant.go index 82d5dcab38..b613974b7e 100644 --- a/internal/command/project_grant.go +++ b/internal/command/project_grant.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/project" @@ -16,69 +17,112 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectGrantWithID(ctx context.Context, grant *domain.ProjectGrant, grantID string, resourceOwner string) (_ *domain.ProjectGrant, err error) { +type AddProjectGrant struct { + es_models.ObjectRoot + + GrantID string + GrantedOrgID string + RoleKeys []string +} + +func (p *AddProjectGrant) IsValid() error { + if p.AggregateID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-FYRnWEzBzV", "Errors.Project.Grant.Invalid") + } + if p.GrantedOrgID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-PPhHpWGRAE", "Errors.Project.Grant.Invalid") + } + return nil +} + +func (c *Commands) AddProjectGrant(ctx context.Context, grant *AddProjectGrant) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - return c.addProjectGrantWithID(ctx, grant, grantID, resourceOwner) -} - -func (c *Commands) AddProjectGrant(ctx context.Context, grant *domain.ProjectGrant, resourceOwner string) (_ *domain.ProjectGrant, err error) { - if !grant.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-3b8fs", "Errors.Project.Grant.Invalid") - } - err = c.checkProjectGrantPreCondition(ctx, grant, resourceOwner) - if err != nil { + if err := grant.IsValid(); err != nil { return nil, err } - grantID, err := c.idGenerator.Next() - if err != nil { - return nil, err - } - - return c.addProjectGrantWithID(ctx, grant, grantID, resourceOwner) -} - -func (c *Commands) addProjectGrantWithID(ctx context.Context, grant *domain.ProjectGrant, grantID string, resourceOwner string) (_ *domain.ProjectGrant, err error) { - grant.GrantID = grantID - - addedGrant := NewProjectGrantWriteModel(grant.GrantID, grant.AggregateID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&addedGrant.WriteModel) - pushedEvents, err := c.eventstore.Push( - ctx, - project.NewGrantAddedEvent(ctx, projectAgg, grant.GrantID, grant.GrantedOrgID, grant.RoleKeys)) - if err != nil { - return nil, err - } - err = AppendAndReduce(addedGrant, pushedEvents...) - if err != nil { - return nil, err - } - return projectGrantWriteModelToProjectGrant(addedGrant), nil -} - -func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *domain.ProjectGrant, resourceOwner string, cascadeUserGrantIDs ...string) (_ *domain.ProjectGrant, err error) { if grant.GrantID == "" { + grant.GrantID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + + projectResourceOwner, err := c.checkProjectGrantPreCondition(ctx, grant.AggregateID, grant.GrantedOrgID, grant.ResourceOwner, grant.RoleKeys) + if err != nil { + return nil, err + } + // if there is no resourceowner provided then use the resourceowner of the project + if grant.ResourceOwner == "" { + grant.ResourceOwner = projectResourceOwner + } + if err := c.checkPermissionUpdateProjectGrant(ctx, grant.ResourceOwner, grant.AggregateID, grant.GrantID); err != nil { + return nil, err + } + + wm := NewProjectGrantWriteModel(grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.ResourceOwner) + // error if provided resourceowner is not equal to the resourceowner of the project or the project grant is for the same organization + if projectResourceOwner != wm.ResourceOwner || wm.ResourceOwner == grant.GrantedOrgID { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-ckUpbvboAH", "Errors.Project.Grant.Invalid") + } + if err := c.pushAppendAndReduce(ctx, + wm, + project.NewGrantAddedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &wm.WriteModel), + grant.GrantID, + grant.GrantedOrgID, + grant.RoleKeys), + ); err != nil { + return nil, err + } + return writeModelToObjectDetails(&wm.WriteModel), nil +} + +type ChangeProjectGrant struct { + es_models.ObjectRoot + + GrantID string + GrantedOrgID string + RoleKeys []string +} + +func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *ChangeProjectGrant, cascadeUserGrantIDs ...string) (_ *domain.ObjectDetails, err error) { + if grant.GrantID == "" && grant.GrantedOrgID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1j83s", "Errors.IDMissing") } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.AggregateID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.ResourceOwner) if err != nil { return nil, err } - grant.GrantedOrgID = existingGrant.GrantedOrgID - err = c.checkProjectGrantPreCondition(ctx, grant, resourceOwner) - if err != nil { - return nil, err + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") } - projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } + projectResourceOwner, err := c.checkProjectGrantPreCondition(ctx, existingGrant.AggregateID, existingGrant.GrantedOrgID, existingGrant.ResourceOwner, grant.RoleKeys) + if err != nil { + return nil, err + } + // error if provided resourceowner is not equal to the resourceowner of the project + if existingGrant.ResourceOwner != projectResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-q1BhA68RBC", "Errors.Project.Grant.Invalid") + } + + // return if there are no changes to the project grant roles if reflect.DeepEqual(existingGrant.RoleKeys, grant.RoleKeys) { - return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0o0pL", "Errors.NoChangesFoundc") + return writeModelToObjectDetails(&existingGrant.WriteModel), nil } events := []eventstore.Command{ - project.NewGrantChangedEvent(ctx, projectAgg, grant.GrantID, grant.RoleKeys), + project.NewGrantChangedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + grant.RoleKeys, + ), } removedRoles := domain.GetRemovedRoles(existingGrant.RoleKeys, grant.RoleKeys) @@ -91,7 +135,7 @@ func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *domain.Project if err != nil { return nil, err } - return projectGrantWriteModelToProjectGrant(existingGrant), nil + return writeModelToObjectDetails(&existingGrant.WriteModel), nil } for _, userGrantID := range cascadeUserGrantIDs { @@ -109,16 +153,16 @@ func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *domain.Project if err != nil { return nil, err } - return projectGrantWriteModelToProjectGrant(existingGrant), nil + return writeModelToObjectDetails(&existingGrant.WriteModel), nil } func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *eventstore.Aggregate, projectID, projectGrantID, roleKey string, cascade bool) (_ eventstore.Command, _ *ProjectGrantWriteModel, err error) { - existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectGrantID, projectID, "") + existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectGrantID, "", projectID, "") if err != nil { return nil, nil, err } - if existingProjectGrant.State == domain.ProjectGrantStateUnspecified || existingProjectGrant.State == domain.ProjectGrantStateRemoved { - return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.Grant.NotFound") + if !existingProjectGrant.State.Exists() { + return nil, nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") } keyExists := false for i, key := range existingProjectGrant.RoleKeys { @@ -133,7 +177,7 @@ func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *e if !keyExists { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5m8g9", "Errors.Project.Grant.RoleKeyNotFound") } - changedProjectGrant := NewProjectGrantWriteModel(projectGrantID, projectID, existingProjectGrant.ResourceOwner) + changedProjectGrant := NewProjectGrantWriteModel(projectGrantID, projectID, "", existingProjectGrant.ResourceOwner) if cascade { return project.NewGrantCascadeChangedEvent(ctx, projectAgg, projectGrantID, existingProjectGrant.RoleKeys), changedProjectGrant, nil @@ -142,26 +186,44 @@ func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *e return project.NewGrantChangedEvent(ctx, projectAgg, projectGrantID, existingProjectGrant.RoleKeys), changedProjectGrant, nil } -func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string) (details *domain.ObjectDetails, err error) { - if grantID == "" || projectID == "" { +func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } - err = c.checkProjectExists(ctx, projectID, resourceOwner) + projectResourceOwner, err := c.checkProjectExists(ctx, projectID, resourceOwner) if err != nil { return nil, err } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) if err != nil { return details, err } + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } + // error if provided resourceowner is not equal to the resourceowner of the project + if projectResourceOwner != existingGrant.ResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Grant.Invalid") + } + // return if project grant is already inactive + if existingGrant.State == domain.ProjectGrantStateInactive { + return writeModelToObjectDetails(&existingGrant.WriteModel), nil + } + // error if project grant is neither active nor inactive if existingGrant.State != domain.ProjectGrantStateActive { return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotActive") } - projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) - - pushedEvents, err := c.eventstore.Push(ctx, project.NewGrantDeactivateEvent(ctx, projectAgg, grantID)) + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, + project.NewGrantDeactivateEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + ), + ) if err != nil { return nil, err } @@ -172,25 +234,44 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string) (details *domain.ObjectDetails, err error) { - if grantID == "" || projectID == "" { +func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } - err = c.checkProjectExists(ctx, projectID, resourceOwner) + projectResourceOwner, err := c.checkProjectExists(ctx, projectID, resourceOwner) if err != nil { return nil, err } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) if err != nil { return details, err } + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } + // error if provided resourceowner is not equal to the resourceowner of the project + if projectResourceOwner != existingGrant.ResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-byscAarAST", "Errors.Project.Grant.Invalid") + } + // return if project grant is already active + if existingGrant.State == domain.ProjectGrantStateActive { + return writeModelToObjectDetails(&existingGrant.WriteModel), nil + } + // error if project grant is neither active nor inactive if existingGrant.State != domain.ProjectGrantStateInactive { return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotInactive") } - projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, project.NewGrantReactivatedEvent(ctx, projectAgg, grantID)) + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, + project.NewGrantReactivatedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + ), + ) if err != nil { return nil, err } @@ -201,22 +282,33 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI return writeModelToObjectDetails(&existingGrant.WriteModel), nil } +// Deprecated: use commands.DeleteProjectGrant func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { if grantID == "" || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing") } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, "", projectID, resourceOwner) if err != nil { return details, err } + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } + if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } events := make([]eventstore.Command, 0) - projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) - events = append(events, project.NewGrantRemovedEvent(ctx, projectAgg, grantID, existingGrant.GrantedOrgID)) + events = append(events, project.NewGrantRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + existingGrant.GrantedOrgID, + ), + ) for _, userGrantID := range cascadeUserGrantIDs { event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) if err != nil { - logging.LogWithFields("COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") continue } events = append(events, event) @@ -232,72 +324,115 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, projectID, resourceOwner string) (member *ProjectGrantWriteModel, err error) { +func (c *Commands) DeleteProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { + return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing") + } + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) + if err != nil { + return details, err + } + // return if project grant does not exist, or was removed already + if !existingGrant.State.Exists() { + return writeModelToObjectDetails(&existingGrant.WriteModel), nil + } + if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } + events := make([]eventstore.Command, 0) + events = append(events, project.NewGrantRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + existingGrant.GrantedOrgID, + ), + ) + + for _, userGrantID := range cascadeUserGrantIDs { + event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) + if err != nil { + logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + continue + } + events = append(events, event) + } + pushedEvents, err := c.eventstore.Push(ctx, events...) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingGrant, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&existingGrant.WriteModel), nil +} + +func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, grantedOrgID, projectID, resourceOwner string) (member *ProjectGrantWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewProjectGrantWriteModel(grantID, projectID, resourceOwner) + writeModel := NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner) err = c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err } - - if writeModel.State == domain.ProjectGrantStateUnspecified || writeModel.State == domain.ProjectGrantStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") - } - return writeModel, nil } -func (c *Commands) checkProjectGrantPreCondition(ctx context.Context, projectGrant *domain.ProjectGrant, resourceOwner string) error { +func (c *Commands) checkProjectGrantPreCondition(ctx context.Context, projectID, grantedOrgID, resourceOwner string, roles []string) (string, error) { if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeProjectGrant) { - return c.checkProjectGrantPreConditionOld(ctx, projectGrant, resourceOwner) + return c.checkProjectGrantPreConditionOld(ctx, projectID, grantedOrgID, resourceOwner, roles) } - existingRoleKeys, err := c.searchProjectGrantState(ctx, projectGrant.AggregateID, projectGrant.GrantedOrgID, resourceOwner) + projectResourceOwner, existingRoleKeys, err := c.searchProjectGrantState(ctx, projectID, grantedOrgID, resourceOwner) if err != nil { - return err + return "", err } - if projectGrant.HasInvalidRoles(existingRoleKeys) { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") + if domain.HasInvalidRoles(existingRoleKeys, roles) { + return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") } - return nil + return projectResourceOwner, nil } -func (c *Commands) searchProjectGrantState(ctx context.Context, projectID, grantedOrgID, resourceOwner string) (existingRoleKeys []string, err error) { +func (c *Commands) searchProjectGrantState(ctx context.Context, projectID, grantedOrgID, resourceOwner string) (_ string, existingRoleKeys []string, err error) { + projectStateQuery := map[eventstore.FieldType]any{ + eventstore.FieldTypeAggregateType: project.AggregateType, + eventstore.FieldTypeAggregateID: projectID, + eventstore.FieldTypeFieldName: project.ProjectStateSearchField, + eventstore.FieldTypeObjectType: project.ProjectSearchType, + } + grantedOrgQuery := map[eventstore.FieldType]any{ + eventstore.FieldTypeAggregateType: org.AggregateType, + eventstore.FieldTypeAggregateID: grantedOrgID, + eventstore.FieldTypeFieldName: org.OrgStateSearchField, + eventstore.FieldTypeObjectType: org.OrgSearchType, + } + roleQuery := map[eventstore.FieldType]any{ + eventstore.FieldTypeAggregateType: project.AggregateType, + eventstore.FieldTypeAggregateID: projectID, + eventstore.FieldTypeFieldName: project.ProjectRoleKeySearchField, + eventstore.FieldTypeObjectType: project.ProjectRoleSearchType, + } + + // as resourceowner is not always provided, it has to be separately + if resourceOwner != "" { + projectStateQuery[eventstore.FieldTypeResourceOwner] = resourceOwner + roleQuery[eventstore.FieldTypeResourceOwner] = resourceOwner + } + results, err := c.eventstore.Search( ctx, - // project state query - map[eventstore.FieldType]any{ - eventstore.FieldTypeResourceOwner: resourceOwner, - eventstore.FieldTypeAggregateType: project.AggregateType, - eventstore.FieldTypeAggregateID: projectID, - eventstore.FieldTypeFieldName: project.ProjectStateSearchField, - eventstore.FieldTypeObjectType: project.ProjectSearchType, - }, - // granted org query - map[eventstore.FieldType]any{ - eventstore.FieldTypeAggregateType: org.AggregateType, - eventstore.FieldTypeAggregateID: grantedOrgID, - eventstore.FieldTypeFieldName: org.OrgStateSearchField, - eventstore.FieldTypeObjectType: org.OrgSearchType, - }, - // role query - map[eventstore.FieldType]any{ - eventstore.FieldTypeResourceOwner: resourceOwner, - eventstore.FieldTypeAggregateType: project.AggregateType, - eventstore.FieldTypeAggregateID: projectID, - eventstore.FieldTypeFieldName: project.ProjectRoleKeySearchField, - eventstore.FieldTypeObjectType: project.ProjectRoleSearchType, - }, + projectStateQuery, + grantedOrgQuery, + roleQuery, ) if err != nil { - return nil, err + return "", nil, err } var ( - existsProject bool - existsGrantedOrg bool + existsProject bool + existingProjectResourceOwner string + existsGrantedOrg bool ) for _, result := range results { @@ -306,31 +441,32 @@ func (c *Commands) searchProjectGrantState(ctx context.Context, projectID, grant var role string err := result.Value.Unmarshal(&role) if err != nil { - return nil, err + return "", nil, err } existingRoleKeys = append(existingRoleKeys, role) case org.OrgSearchType: var state domain.OrgState err := result.Value.Unmarshal(&state) if err != nil { - return nil, err + return "", nil, err } existsGrantedOrg = state.Valid() && state != domain.OrgStateRemoved case project.ProjectSearchType: var state domain.ProjectState err := result.Value.Unmarshal(&state) if err != nil { - return nil, err + return "", nil, err } existsProject = state.Valid() && state != domain.ProjectStateRemoved + existingProjectResourceOwner = result.Aggregate.ResourceOwner } } if !existsProject { - return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") + return "", nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") } if !existsGrantedOrg { - return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") + return "", nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") } - return existingRoleKeys, nil + return existingProjectResourceOwner, existingRoleKeys, nil } diff --git a/internal/command/project_grant_model.go b/internal/command/project_grant_model.go index c658b00b69..15950d4f3d 100644 --- a/internal/command/project_grant_model.go +++ b/internal/command/project_grant_model.go @@ -16,13 +16,14 @@ type ProjectGrantWriteModel struct { State domain.ProjectGrantState } -func NewProjectGrantWriteModel(grantID, projectID, resourceOwner string) *ProjectGrantWriteModel { +func NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner string) *ProjectGrantWriteModel { return &ProjectGrantWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: projectID, ResourceOwner: resourceOwner, }, - GrantID: grantID, + GrantID: grantID, + GrantedOrgID: grantedOrgID, } } @@ -30,27 +31,28 @@ func (wm *ProjectGrantWriteModel) AppendEvents(events ...eventstore.Event) { for _, event := range events { switch e := event.(type) { case *project.GrantAddedEvent: - if e.GrantID == wm.GrantID { + if (wm.GrantID != "" && e.GrantID == wm.GrantID) || + (wm.GrantedOrgID != "" && e.GrantedOrgID == wm.GrantedOrgID) { wm.WriteModel.AppendEvents(e) } case *project.GrantChangedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantCascadeChangedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantDeactivateEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantReactivatedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantRemovedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.ProjectRemovedEvent: @@ -114,18 +116,20 @@ func (wm *ProjectGrantWriteModel) Query() *eventstore.SearchQueryBuilder { type ProjectGrantPreConditionReadModel struct { eventstore.WriteModel - ProjectID string - GrantedOrgID string - ProjectExists bool - GrantedOrgExists bool - ExistingRoleKeys []string + ProjectResourceOwner string + ProjectID string + GrantedOrgID string + ProjectExists bool + GrantedOrgExists bool + ExistingRoleKeys []string } func NewProjectGrantPreConditionReadModel(projectID, grantedOrgID, resourceOwner string) *ProjectGrantPreConditionReadModel { return &ProjectGrantPreConditionReadModel{ - WriteModel: eventstore.WriteModel{ResourceOwner: resourceOwner}, - ProjectID: projectID, - GrantedOrgID: grantedOrgID, + WriteModel: eventstore.WriteModel{}, + ProjectResourceOwner: resourceOwner, + ProjectID: projectID, + GrantedOrgID: grantedOrgID, } } @@ -133,22 +137,26 @@ func (wm *ProjectGrantPreConditionReadModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { case *project.ProjectAddedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if wm.ProjectResourceOwner == "" { + wm.ProjectResourceOwner = e.Aggregate().ResourceOwner + } + if wm.ProjectResourceOwner != e.Aggregate().ResourceOwner { continue } wm.ProjectExists = true case *project.ProjectRemovedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if wm.ProjectResourceOwner != e.Aggregate().ResourceOwner { continue } + wm.ProjectResourceOwner = "" wm.ProjectExists = false case *project.RoleAddedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if e.Aggregate().ResourceOwner != wm.ProjectResourceOwner { continue } wm.ExistingRoleKeys = append(wm.ExistingRoleKeys, e.Key) case *project.RoleRemovedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if e.Aggregate().ResourceOwner != wm.ProjectResourceOwner { continue } for i, key := range wm.ExistingRoleKeys { @@ -171,12 +179,6 @@ func (wm *ProjectGrantPreConditionReadModel) Reduce() error { func (wm *ProjectGrantPreConditionReadModel) Query() *eventstore.SearchQueryBuilder { query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). AddQuery(). - AggregateTypes(org.AggregateType). - AggregateIDs(wm.GrantedOrgID). - EventTypes( - org.OrgAddedEventType, - org.OrgRemovedEventType). - Or(). AggregateTypes(project.AggregateType). AggregateIDs(wm.ProjectID). EventTypes( @@ -184,6 +186,12 @@ func (wm *ProjectGrantPreConditionReadModel) Query() *eventstore.SearchQueryBuil project.ProjectRemovedType, project.RoleAddedType, project.RoleRemovedType). + Or(). + AggregateTypes(org.AggregateType). + AggregateIDs(wm.GrantedOrgID). + EventTypes( + org.OrgAddedEventType, + org.OrgRemovedEventType). Builder() return query diff --git a/internal/command/project_grant_test.go b/internal/command/project_grant_test.go index e39835e2f4..7a3bb98e7d 100644 --- a/internal/command/project_grant_test.go +++ b/internal/command/project_grant_test.go @@ -19,16 +19,16 @@ import ( func TestCommandSide_AddProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - projectGrant *domain.ProjectGrant - resourceOwner string + ctx context.Context + projectGrant *AddProjectGrant } type res struct { - want *domain.ProjectGrant + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -40,18 +40,18 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "invalid usergrant, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -60,20 +60,21 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", GrantedOrgID: "grantedorg1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -82,8 +83,7 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "project not existing in org, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -108,16 +108,18 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", GrantedOrgID: "grantedorg1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -126,8 +128,7 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "granted org not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -138,16 +139,18 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", GrantedOrgID: "grantedorg1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -156,8 +159,7 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "project roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -174,27 +176,74 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", GrantedOrgID: "grantedorg1", RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, }, }, { - name: "usergrant for project, ok", + name: "grant for project, same resourceowner", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "projectgrant1"), + }, + args: args{ + ctx: context.Background(), + projectGrant: &AddProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + GrantedOrgID: "org1", + RoleKeys: []string{"key1"}, + }, + }, + res: res{ + err: zerrors.IsPreconditionFailed, + }, + }, + { + name: "grant for project, ok", + fields: fields{ + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -227,21 +276,67 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "projectgrant1"), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "projectgrant1"), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, GrantedOrgID: "grantedorg1", RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ - want: &domain.ProjectGrant{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "grant for project, id, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + ), + expectPush( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", @@ -249,7 +344,11 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { GrantID: "projectgrant1", GrantedOrgID: "grantedorg1", RoleKeys: []string{"key1"}, - State: domain.ProjectGrantStateActive, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -257,10 +356,11 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + checkPermission: tt.fields.checkPermission, } - got, err := r.AddProjectGrant(tt.args.ctx, tt.args.projectGrant, tt.args.resourceOwner) + got, err := r.AddProjectGrant(tt.args.ctx, tt.args.projectGrant) if tt.res.err == nil { assert.NoError(t, err) } @@ -268,7 +368,8 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assert.NotEmpty(t, got.ID) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -276,16 +377,16 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { func TestCommandSide_ChangeProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context - projectGrant *domain.ProjectGrant - resourceOwner string + projectGrant *ChangeProjectGrant cascadeUserGrantIDs []string } type res struct { - want *domain.ProjectGrant + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -297,18 +398,17 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "invalid projectgrant, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -317,22 +417,21 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "projectgrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsNotFound, @@ -341,8 +440,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -353,17 +451,17 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", + GrantID: "projectgrant1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -372,8 +470,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "project not existing in org, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -398,18 +495,18 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -418,8 +515,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "granted org not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -438,17 +534,17 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", + GrantID: "projectgrant1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -457,8 +553,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "project roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -483,18 +578,18 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -503,8 +598,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "projectgrant not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -537,28 +631,29 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "projectgrant only added roles, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -606,37 +701,99 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1", "key2"}, - }, - resourceOwner: "org1", - }, - res: res{ - want: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + GrantID: "projectgrant1", + RoleKeys: []string{"key1", "key2"}, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant only added roles, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key2", + "key2", + "", + ), + ), + ), + expectPush( + project.NewGrantChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + []string{"key1", "key2"}, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", }, - GrantID: "projectgrant1", GrantedOrgID: "grantedorg1", RoleKeys: []string{"key1", "key2"}, - State: domain.ProjectGrantStateActive, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, { name: "projectgrant remove roles, usergrant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -685,38 +842,30 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, - }, - resourceOwner: "org1", - cascadeUserGrantIDs: []string{"usergrant1"}, - }, - res: res{ - want: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, - State: domain.ProjectGrantStateActive, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, + }, + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, { name: "projectgrant remove roles, usergrant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -778,30 +927,23 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, - }, - resourceOwner: "org1", - cascadeUserGrantIDs: []string{"usergrant1"}, - }, - res: res{ - want: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, - State: domain.ProjectGrantStateActive, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, + }, + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -809,9 +951,10 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProjectGrant(tt.args.ctx, tt.args.projectGrant, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) + got, err := r.ChangeProjectGrant(tt.args.ctx, tt.args.projectGrant, tt.args.cascadeUserGrantIDs...) if tt.res.err == nil { assert.NoError(t, err) } @@ -819,7 +962,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -827,12 +970,14 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { func TestCommandSide_DeactivateProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context projectID string grantID string + grantedOrgID string resourceOwner string } type res struct { @@ -848,9 +993,8 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { { name: "missing projectid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -864,9 +1008,8 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { { name: "missing grantid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -880,10 +1023,10 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { { name: "project not existing, precondition failed error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -898,8 +1041,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { { name: "projectgrant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -911,6 +1053,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -923,10 +1066,9 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { }, }, { - name: "projectgrant already deactivated, precondition error", + name: "projectgrant already deactivated, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -949,6 +1091,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { )), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -957,14 +1100,15 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "projectgrant deactivate, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -989,6 +1133,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1002,13 +1147,56 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant deactivate, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.DeactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) + got, err := r.DeactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -1024,12 +1212,14 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { func TestCommandSide_ReactivateProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context projectID string grantID string + grantedOrgID string resourceOwner string } type res struct { @@ -1045,9 +1235,8 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "missing projectid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1061,9 +1250,8 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "missing grantid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1077,10 +1265,10 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "project not existing, precondition failed error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1095,8 +1283,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "projectgrant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1108,6 +1295,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1122,8 +1310,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "projectgrant not inactive, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1142,6 +1329,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { )), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1150,14 +1338,15 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "projectgrant reactivate, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1186,6 +1375,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1199,13 +1389,60 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant reactivate, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + eventFromEventPusher(project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + )), + ), + expectPush( + project.NewGrantReactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.ReactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) + got, err := r.ReactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -1221,7 +1458,8 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { func TestCommandSide_RemoveProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -1243,9 +1481,8 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "missing projectid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1259,9 +1496,8 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "missing grantid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1275,8 +1511,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "project already removed, precondition failed error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -1293,6 +1528,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1307,10 +1543,10 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "projectgrant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1325,8 +1561,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "projectgrant remove, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -1343,6 +1578,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1359,8 +1595,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "projectgrant remove, cascading usergrant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -1378,6 +1613,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1395,8 +1631,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "projectgrant remove with cascading usergrants, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -1426,6 +1661,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1444,7 +1680,8 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) if tt.res.err == nil { @@ -1459,3 +1696,283 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { }) } } + +func TestCommandSide_DeleteProjectGrant(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + projectID string + grantID string + grantedOrgID string + resourceOwner string + cascadeUserGrantIDs []string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing projectid, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "missing grantid, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "project already removed, precondition failed error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + nil, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant not existing, precondition error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, cascading usergrant not found, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter(), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove with cascading usergrants, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher(usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + []string{"key1"}))), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + usergrant.NewUserGrantCascadeRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.DeleteProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) + 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.want, got) + } + }) + } +} diff --git a/internal/command/project_model.go b/internal/command/project_model.go index a46b07a8fe..cabceb8500 100644 --- a/internal/command/project_model.go +++ b/internal/command/project_model.go @@ -88,40 +88,35 @@ func (wm *ProjectWriteModel) Query() *eventstore.SearchQueryBuilder { func (wm *ProjectWriteModel) NewChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - name string, + name *string, projectRoleAssertion, projectRoleCheck, - hasProjectCheck bool, - privateLabelingSetting domain.PrivateLabelingSetting, -) (*project.ProjectChangeEvent, bool, error) { + hasProjectCheck *bool, + privateLabelingSetting *domain.PrivateLabelingSetting, +) *project.ProjectChangeEvent { changes := make([]project.ProjectChanges, 0) - var err error oldName := "" - if wm.Name != name { + if name != nil && wm.Name != *name { oldName = wm.Name - changes = append(changes, project.ChangeName(name)) + changes = append(changes, project.ChangeName(*name)) } - if wm.ProjectRoleAssertion != projectRoleAssertion { - changes = append(changes, project.ChangeProjectRoleAssertion(projectRoleAssertion)) + if projectRoleAssertion != nil && wm.ProjectRoleAssertion != *projectRoleAssertion { + changes = append(changes, project.ChangeProjectRoleAssertion(*projectRoleAssertion)) } - if wm.ProjectRoleCheck != projectRoleCheck { - changes = append(changes, project.ChangeProjectRoleCheck(projectRoleCheck)) + if projectRoleCheck != nil && wm.ProjectRoleCheck != *projectRoleCheck { + changes = append(changes, project.ChangeProjectRoleCheck(*projectRoleCheck)) } - if wm.HasProjectCheck != hasProjectCheck { - changes = append(changes, project.ChangeHasProjectCheck(hasProjectCheck)) + if hasProjectCheck != nil && wm.HasProjectCheck != *hasProjectCheck { + changes = append(changes, project.ChangeHasProjectCheck(*hasProjectCheck)) } - if wm.PrivateLabelingSetting != privateLabelingSetting { - changes = append(changes, project.ChangePrivateLabelingSetting(privateLabelingSetting)) + if privateLabelingSetting != nil && wm.PrivateLabelingSetting != *privateLabelingSetting { + changes = append(changes, project.ChangePrivateLabelingSetting(*privateLabelingSetting)) } if len(changes) == 0 { - return nil, false, nil + return nil } - changeEvent, err := project.NewProjectChangeEvent(ctx, aggregate, oldName, changes) - if err != nil { - return nil, false, err - } - return changeEvent, true, nil + return project.NewProjectChangeEvent(ctx, aggregate, oldName, changes) } func isProjectStateExists(state domain.ProjectState) bool { @@ -132,6 +127,10 @@ func ProjectAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggre return eventstore.AggregateFromWriteModel(wm, project.AggregateType, project.AggregateVersion) } +func ProjectAggregateFromWriteModelWithCTX(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return project.AggregateFromWriteModel(ctx, wm) +} + func hasProjectState(check domain.ProjectState, states ...domain.ProjectState) bool { for _, state := range states { if check == state { diff --git a/internal/command/project_old.go b/internal/command/project_old.go index 35ea9b3ebb..99d7dd2e34 100644 --- a/internal/command/project_old.go +++ b/internal/command/project_old.go @@ -9,18 +9,18 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) checkProjectExistsOld(ctx context.Context, projectID, resourceOwner string) (err error) { +func (c *Commands) checkProjectExistsOld(ctx context.Context, projectID, resourceOwner string) (_ string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() projectWriteModel, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner) if err != nil { - return err + return "", err } if !isProjectStateExists(projectWriteModel.State) { - return zerrors.ThrowPreconditionFailed(nil, "COMMAND-EbFMN", "Errors.Project.NotFound") + return "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-EbFMN", "Errors.Project.NotFound") } - return nil + return projectWriteModel.ResourceOwner, nil } func (c *Commands) deactivateProjectOld(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) { @@ -34,6 +34,9 @@ func (c *Commands) deactivateProjectOld(ctx context.Context, projectID string, r if existingProject.State != domain.ProjectStateActive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-mki55", "Errors.Project.NotActive") } + if err := c.checkPermissionUpdateProject(ctx, existingProject.ResourceOwner, existingProject.AggregateID); err != nil { + return nil, err + } //nolint: contextcheck projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) @@ -59,6 +62,9 @@ func (c *Commands) reactivateProjectOld(ctx context.Context, projectID string, r if existingProject.State != domain.ProjectStateInactive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M9bs", "Errors.Project.NotInactive") } + if err := c.checkPermissionUpdateProject(ctx, existingProject.ResourceOwner, existingProject.AggregateID); err != nil { + return nil, err + } //nolint: contextcheck projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) @@ -73,20 +79,20 @@ func (c *Commands) reactivateProjectOld(ctx context.Context, projectID string, r return writeModelToObjectDetails(&existingProject.WriteModel), nil } -func (c *Commands) checkProjectGrantPreConditionOld(ctx context.Context, projectGrant *domain.ProjectGrant, resourceOwner string) error { - preConditions := NewProjectGrantPreConditionReadModel(projectGrant.AggregateID, projectGrant.GrantedOrgID, resourceOwner) +func (c *Commands) checkProjectGrantPreConditionOld(ctx context.Context, projectID, grantedOrgID, resourceOwner string, roles []string) (string, error) { + preConditions := NewProjectGrantPreConditionReadModel(projectID, grantedOrgID, resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, preConditions) if err != nil { - return err + return "", err } if !preConditions.ProjectExists { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") + return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") } if !preConditions.GrantedOrgExists { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") + return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") } - if projectGrant.HasInvalidRoles(preConditions.ExistingRoleKeys) { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") + if domain.HasInvalidRoles(preConditions.ExistingRoleKeys, roles) { + return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") } - return nil + return preConditions.ProjectResourceOwner, nil } diff --git a/internal/command/project_role.go b/internal/command/project_role.go index 3c8f9c725c..60d002b967 100644 --- a/internal/command/project_role.go +++ b/internal/command/project_role.go @@ -7,22 +7,45 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectRole(ctx context.Context, projectRole *domain.ProjectRole, resourceOwner string) (_ *domain.ProjectRole, err error) { +type AddProjectRole struct { + models.ObjectRoot + + Key string + DisplayName string + Group string +} + +func (p *AddProjectRole) IsValid() bool { + return p.AggregateID != "" && p.Key != "" +} + +func (c *Commands) AddProjectRole(ctx context.Context, projectRole *AddProjectRole) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - err = c.checkProjectExists(ctx, projectRole.AggregateID, resourceOwner) + projectResourceOwner, err := c.checkProjectExists(ctx, projectRole.AggregateID, projectRole.ResourceOwner) if err != nil { return nil, err } + if projectRole.ResourceOwner == "" { + projectRole.ResourceOwner = projectResourceOwner + } + if err := c.checkPermissionWriteProjectRole(ctx, projectRole.ResourceOwner, projectRole.Key); err != nil { + return nil, err + } - roleWriteModel := NewProjectRoleWriteModelWithKey(projectRole.Key, projectRole.AggregateID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&roleWriteModel.WriteModel) + roleWriteModel := NewProjectRoleWriteModelWithKey(projectRole.Key, projectRole.AggregateID, projectRole.ResourceOwner) + if roleWriteModel.ResourceOwner != projectResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-RLB4UpqQSd", "Errors.Project.Role.Invalid") + } + + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &roleWriteModel.WriteModel) events, err := c.addProjectRoles(ctx, projectAgg, projectRole) if err != nil { return nil, err @@ -35,37 +58,45 @@ func (c *Commands) AddProjectRole(ctx context.Context, projectRole *domain.Proje if err != nil { return nil, err } - return roleWriteModelToRole(roleWriteModel), nil + return writeModelToObjectDetails(&roleWriteModel.WriteModel), nil } -func (c *Commands) BulkAddProjectRole(ctx context.Context, projectID, resourceOwner string, projectRoles []*domain.ProjectRole) (details *domain.ObjectDetails, err error) { - err = c.checkProjectExists(ctx, projectID, resourceOwner) +func (c *Commands) checkPermissionWriteProjectRole(ctx context.Context, resourceOwner, roleKey string) error { + return c.checkPermission(ctx, domain.PermissionProjectRoleWrite, resourceOwner, roleKey) +} + +func (c *Commands) BulkAddProjectRole(ctx context.Context, projectID, resourceOwner string, projectRoles []*AddProjectRole) (details *domain.ObjectDetails, err error) { + projectResourceOwner, err := c.checkProjectExists(ctx, projectID, resourceOwner) if err != nil { return nil, err } + for _, projectRole := range projectRoles { + if projectRole.ResourceOwner == "" { + projectRole.ResourceOwner = projectResourceOwner + } + if err := c.checkPermissionWriteProjectRole(ctx, projectRole.ResourceOwner, projectRole.Key); err != nil { + return nil, err + } + if projectRole.ResourceOwner != projectResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-9ZXtdaJKJJ", "Errors.Project.Role.Invalid") + } + } roleWriteModel := NewProjectRoleWriteModel(projectID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&roleWriteModel.WriteModel) + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &roleWriteModel.WriteModel) events, err := c.addProjectRoles(ctx, projectAgg, projectRoles...) if err != nil { return details, err } - - pushedEvents, err := c.eventstore.Push(ctx, events...) - if err != nil { - return nil, err - } - err = AppendAndReduce(roleWriteModel, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&roleWriteModel.WriteModel), nil + return c.pushAppendAndReduceDetails(ctx, roleWriteModel, events...) } -func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.Aggregate, projectRoles ...*domain.ProjectRole) ([]eventstore.Command, error) { +func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.Aggregate, projectRoles ...*AddProjectRole) ([]eventstore.Command, error) { var events []eventstore.Command for _, projectRole := range projectRoles { - projectRole.AggregateID = projectAgg.ID + if projectRole.ResourceOwner != projectAgg.ResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-4Q2WjlbHvc", "Errors.Project.Role.Invalid") + } if !projectRole.IsValid() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Role.Invalid") } @@ -81,42 +112,55 @@ func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.A return events, nil } -func (c *Commands) ChangeProjectRole(ctx context.Context, projectRole *domain.ProjectRole, resourceOwner string) (_ *domain.ProjectRole, err error) { +type ChangeProjectRole struct { + models.ObjectRoot + + Key string + DisplayName string + Group string +} + +func (p *ChangeProjectRole) IsValid() bool { + return p.AggregateID != "" && p.Key != "" +} + +func (c *Commands) ChangeProjectRole(ctx context.Context, projectRole *ChangeProjectRole) (_ *domain.ObjectDetails, err error) { if !projectRole.IsValid() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2ilfW", "Errors.Project.Invalid") } - err = c.checkProjectExists(ctx, projectRole.AggregateID, resourceOwner) + projectResourceOwner, err := c.checkProjectExists(ctx, projectRole.AggregateID, projectRole.ResourceOwner) if err != nil { return nil, err } + if projectRole.ResourceOwner == "" { + projectRole.ResourceOwner = projectResourceOwner + } + if err := c.checkPermissionWriteProjectRole(ctx, projectRole.ResourceOwner, projectRole.Key); err != nil { + return nil, err + } - existingRole, err := c.getProjectRoleWriteModelByID(ctx, projectRole.Key, projectRole.AggregateID, resourceOwner) + existingRole, err := c.getProjectRoleWriteModelByID(ctx, projectRole.Key, projectRole.AggregateID, projectRole.ResourceOwner) if err != nil { return nil, err } - if existingRole.State == domain.ProjectRoleStateUnspecified || existingRole.State == domain.ProjectRoleStateRemoved { + if !existingRole.State.Exists() { return nil, zerrors.ThrowNotFound(nil, "COMMAND-vv8M9", "Errors.Project.Role.NotExisting") } + if existingRole.ResourceOwner != projectResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-3MizLWveMf", "Errors.Project.Role.Invalid") + } - projectAgg := ProjectAggregateFromWriteModel(&existingRole.WriteModel) + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingRole.WriteModel) changeEvent, changed, err := existingRole.NewProjectRoleChangedEvent(ctx, projectAgg, projectRole.Key, projectRole.DisplayName, projectRole.Group) if err != nil { return nil, err } if !changed { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M0cs", "Errors.NoChangesFound") + return writeModelToObjectDetails(&existingRole.WriteModel), nil } - pushedEvents, err := c.eventstore.Push(ctx, changeEvent) - if err != nil { - return nil, err - } - err = AppendAndReduce(existingRole, pushedEvents...) - if err != nil { - return nil, err - } - return roleWriteModelToRole(existingRole), nil + return c.pushAppendAndReduceDetails(ctx, existingRole, changeEvent) } func (c *Commands) RemoveProjectRole(ctx context.Context, projectID, key, resourceOwner string, cascadingProjectGrantIds []string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { @@ -127,10 +171,14 @@ func (c *Commands) RemoveProjectRole(ctx context.Context, projectID, key, resour if err != nil { return details, err } - if existingRole.State == domain.ProjectRoleStateUnspecified || existingRole.State == domain.ProjectRoleStateRemoved { - return details, zerrors.ThrowNotFound(nil, "COMMAND-m9vMf", "Errors.Project.Role.NotExisting") + // return if project role is not existing + if !existingRole.State.Exists() { + return writeModelToObjectDetails(&existingRole.WriteModel), nil } - projectAgg := ProjectAggregateFromWriteModel(&existingRole.WriteModel) + if err := c.checkPermissionDeleteProjectRole(ctx, existingRole.ResourceOwner, existingRole.Key); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingRole.WriteModel) events := []eventstore.Command{ project.NewRoleRemovedEvent(ctx, projectAgg, key), } @@ -153,15 +201,11 @@ func (c *Commands) RemoveProjectRole(ctx context.Context, projectID, key, resour events = append(events, event) } - pushedEvents, err := c.eventstore.Push(ctx, events...) - if err != nil { - return nil, err - } - err = AppendAndReduce(existingRole, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&existingRole.WriteModel), nil + return c.pushAppendAndReduceDetails(ctx, existingRole, events...) +} + +func (c *Commands) checkPermissionDeleteProjectRole(ctx context.Context, resourceOwner, roleKey string) error { + return c.checkPermission(ctx, domain.PermissionProjectRoleDelete, resourceOwner, roleKey) } func (c *Commands) getProjectRoleWriteModelByID(ctx context.Context, key, projectID, resourceOwner string) (*ProjectRoleWriteModel, error) { diff --git a/internal/command/project_role_model.go b/internal/command/project_role_model.go index 641879f238..3fc9a6c814 100644 --- a/internal/command/project_role_model.go +++ b/internal/command/project_role_model.go @@ -17,6 +17,10 @@ type ProjectRoleWriteModel struct { State domain.ProjectRoleState } +func (wm *ProjectRoleWriteModel) GetWriteModel() *eventstore.WriteModel { + return &wm.WriteModel +} + func NewProjectRoleWriteModelWithKey(key, projectID, resourceOwner string) *ProjectRoleWriteModel { return &ProjectRoleWriteModel{ WriteModel: eventstore.WriteModel{ diff --git a/internal/command/project_role_test.go b/internal/command/project_role_test.go index 2dc7ee35a6..14db198270 100644 --- a/internal/command/project_role_test.go +++ b/internal/command/project_role_test.go @@ -16,15 +16,15 @@ import ( func TestCommandSide_AddProjectRole(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - role *domain.ProjectRole - resourceOwner string + ctx context.Context + role *AddProjectRole } type res struct { - want *domain.ProjectRole + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -36,8 +36,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -55,16 +54,16 @@ func TestCommandSide_AddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, Key: "key1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -73,8 +72,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { { name: "invalid role, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -85,15 +83,15 @@ func TestCommandSide_AddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -102,8 +100,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { { name: "role key already exists, already exists error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -123,10 +120,11 @@ func TestCommandSide_AddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -134,7 +132,6 @@ func TestCommandSide_AddProjectRole(t *testing.T) { DisplayName: "key", Group: "group", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -143,8 +140,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { { name: "add role,ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -164,10 +160,11 @@ func TestCommandSide_AddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -175,10 +172,41 @@ func TestCommandSide_AddProjectRole(t *testing.T) { DisplayName: "key", Group: "group", }, - resourceOwner: "org1", }, res: res{ - want: &domain.ProjectRole{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "add role, resourceowner, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectPush( + project.NewRoleAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", @@ -188,14 +216,20 @@ func TestCommandSide_AddProjectRole(t *testing.T) { Group: "group", }, }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.AddProjectRole(tt.args.ctx, tt.args.role, tt.args.resourceOwner) + got, err := r.AddProjectRole(tt.args.ctx, tt.args.role) if tt.res.err == nil { assert.NoError(t, err) } @@ -203,7 +237,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -211,11 +245,12 @@ func TestCommandSide_AddProjectRole(t *testing.T) { func TestCommandSide_BulkAddProjectRole(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context - roles []*domain.ProjectRole + roles []*AddProjectRole projectID string resourceOwner string } @@ -232,8 +267,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -251,10 +285,11 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - roles: []*domain.ProjectRole{ + roles: []*AddProjectRole{ { ObjectRoot: models.ObjectRoot{ AggregateID: "project1", @@ -271,8 +306,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { { name: "invalid role, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -283,10 +317,11 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - roles: []*domain.ProjectRole{ + roles: []*AddProjectRole{ { ObjectRoot: models.ObjectRoot{}, }, @@ -304,8 +339,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { { name: "role key already exists, already exists error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -332,16 +366,23 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - roles: []*domain.ProjectRole{ + roles: []*AddProjectRole{ { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, Key: "key1", DisplayName: "key", Group: "group", }, { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, Key: "key2", DisplayName: "key2", Group: "group", @@ -357,8 +398,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { { name: "add roles,ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -385,16 +425,23 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - roles: []*domain.ProjectRole{ + roles: []*AddProjectRole{ { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, Key: "key1", DisplayName: "key", Group: "group", }, { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, Key: "key2", DisplayName: "key2", Group: "group", @@ -413,7 +460,8 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.BulkAddProjectRole(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner, tt.args.roles) if tt.res.err == nil { @@ -431,15 +479,15 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { func TestCommandSide_ChangeProjectRole(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - role *domain.ProjectRole - resourceOwner string + ctx context.Context + role *ChangeProjectRole } type res struct { - want *domain.ProjectRole + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -451,18 +499,16 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { { name: "invalid role, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -471,8 +517,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -490,16 +535,16 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, Key: "key1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -508,8 +553,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { { name: "role removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -536,10 +580,11 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -547,7 +592,6 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { DisplayName: "key", Group: "group", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsNotFound, @@ -556,8 +600,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { { name: "role not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -578,10 +621,11 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -589,17 +633,17 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { DisplayName: "key", Group: "group", }, - resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "role changed, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -623,10 +667,11 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { newRoleChangedEvent(context.Background(), "project1", "org1", "key1", "keychanged", "groupchanged"), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -634,17 +679,10 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { DisplayName: "keychanged", Group: "groupchanged", }, - resourceOwner: "org1", }, res: res{ - want: &domain.ProjectRole{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - ResourceOwner: "org1", - }, - Key: "key1", - DisplayName: "keychanged", - Group: "groupchanged", + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -652,9 +690,10 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProjectRole(tt.args.ctx, tt.args.role, tt.args.resourceOwner) + got, err := r.ChangeProjectRole(tt.args.ctx, tt.args.role) if tt.res.err == nil { assert.NoError(t, err) } @@ -662,7 +701,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -670,7 +709,8 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { func TestCommandSide_RemoveProjectRole(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -693,9 +733,8 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "invalid projectid, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -709,9 +748,8 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "invalid key, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -726,10 +764,10 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -738,14 +776,15 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsNotFound, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "role removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -763,6 +802,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -771,14 +811,15 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsNotFound, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "role removed, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -796,6 +837,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -812,8 +854,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role removed with cascadingProjectGrantids, grant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -832,6 +873,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -849,8 +891,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role removed with cascadingProjectGrantids, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -883,6 +924,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -900,8 +942,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role removed with cascadingUserGrantIDs, grant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -920,6 +961,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -937,8 +979,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role removed with cascadingUserGrantIDs, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -969,6 +1010,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -987,7 +1029,8 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveProjectRole(tt.args.ctx, tt.args.projectID, tt.args.key, tt.args.resourceOwner, tt.args.cascadingProjectGrantIDs, tt.args.cascadingUserGrantIDs...) if tt.res.err == nil { diff --git a/internal/command/project_test.go b/internal/command/project_test.go index 842e1aa640..6c03420f6b 100644 --- a/internal/command/project_test.go +++ b/internal/command/project_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/api/authz" @@ -18,16 +19,16 @@ import ( func TestCommandSide_AddProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - project *domain.Project - resourceOwner string + ctx context.Context + project *AddProject } type res struct { - want *domain.Project + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -39,14 +40,14 @@ func TestCommandSide_AddProject(t *testing.T) { { name: "invalid project, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{}, - resourceOwner: "org1", + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + project: &AddProject{ + ObjectRoot: models.ObjectRoot{ResourceOwner: "org1"}, + }, }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -55,62 +56,51 @@ func TestCommandSide_AddProject(t *testing.T) { { name: "project, resourceowner empty", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{ + project: &AddProject{ + ObjectRoot: models.ObjectRoot{ResourceOwner: ""}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, HasProjectCheck: true, PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, - resourceOwner: "", }, res: res{ err: zerrors.IsErrorInvalidArgument, }, }, { - name: "project, error already exists", + name: "project, no permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), - expectPushFailed(zerrors.ThrowAlreadyExists(nil, "ERROR", "internl"), - project.NewProjectAddedEvent( - context.Background(), - &project.NewAggregate("project1", "org1").Aggregate, - "project", true, true, true, - domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, - ), - ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), + checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{ + project: &AddProject{ + ObjectRoot: models.ObjectRoot{AggregateID: "project1", ResourceOwner: "org1"}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, HasProjectCheck: true, PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, - resourceOwner: "org1", }, res: res{ - err: zerrors.IsErrorAlreadyExists, + err: zerrors.IsPermissionDenied, }, }, { name: "project, already exists", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -120,18 +110,19 @@ func TestCommandSide_AddProject(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{ + project: &AddProject{ + ObjectRoot: models.ObjectRoot{ResourceOwner: "org1"}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, HasProjectCheck: true, PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -140,8 +131,7 @@ func TestCommandSide_AddProject(t *testing.T) { { name: "project, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), expectPush( project.NewProjectAddedEvent( @@ -152,25 +142,46 @@ func TestCommandSide_AddProject(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{ + project: &AddProject{ + ObjectRoot: models.ObjectRoot{ResourceOwner: "org1"}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, HasProjectCheck: true, PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, - resourceOwner: "org1", }, res: res{ - want: &domain.Project{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "project1", - }, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "project, with id, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + project: &AddProject{ + ObjectRoot: models.ObjectRoot{AggregateID: "project1", ResourceOwner: "org1"}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, @@ -178,16 +189,22 @@ func TestCommandSide_AddProject(t *testing.T) { PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + checkPermission: tt.fields.checkPermission, } c.setMilestonesCompletedForTest("instanceID") - got, err := c.AddProject(tt.args.ctx, tt.args.project, tt.args.resourceOwner) + got, err := c.AddProject(tt.args.ctx, tt.args.project) if tt.res.err == nil { assert.NoError(t, err) } @@ -195,7 +212,8 @@ func TestCommandSide_AddProject(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assert.NotEmpty(t, got.ID) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -203,15 +221,16 @@ func TestCommandSide_AddProject(t *testing.T) { func TestCommandSide_ChangeProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context - project *domain.Project + project *ChangeProject resourceOwner string } type res struct { - want *domain.Project + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -223,16 +242,16 @@ func TestCommandSide_ChangeProject(t *testing.T) { { name: "invalid project, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, + Name: gu.Ptr(""), }, resourceOwner: "org1", }, @@ -243,14 +262,13 @@ func TestCommandSide_ChangeProject(t *testing.T) { { name: "invalid project empty aggregateid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ - Name: "project", + project: &ChangeProject{ + Name: gu.Ptr("project"), }, resourceOwner: "org1", }, @@ -261,18 +279,18 @@ func TestCommandSide_ChangeProject(t *testing.T) { { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project change", + Name: gu.Ptr("project change"), }, resourceOwner: "org1", }, @@ -283,8 +301,7 @@ func TestCommandSide_ChangeProject(t *testing.T) { { name: "project removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -300,14 +317,15 @@ func TestCommandSide_ChangeProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project change", + Name: gu.Ptr("project change"), }, resourceOwner: "org1", }, @@ -316,10 +334,9 @@ func TestCommandSide_ChangeProject(t *testing.T) { }, }, { - name: "no changes, precondition error", + name: "no changes, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -329,30 +346,65 @@ func TestCommandSide_ChangeProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project", - ProjectRoleAssertion: true, - ProjectRoleCheck: true, - HasProjectCheck: true, - PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + Name: gu.Ptr("project"), + ProjectRoleAssertion: gu.Ptr(true), + ProjectRoleCheck: gu.Ptr(true), + HasProjectCheck: gu.Ptr(true), + PrivateLabelingSetting: gu.Ptr(domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), }, resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "no changes, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + project: &ChangeProject{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Name: gu.Ptr("project"), + ProjectRoleAssertion: gu.Ptr(true), + ProjectRoleCheck: gu.Ptr(true), + HasProjectCheck: gu.Ptr(true), + PrivateLabelingSetting: gu.Ptr(domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + }, + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsPermissionDenied, }, }, { name: "project change with name and unique constraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -374,40 +426,32 @@ func TestCommandSide_ChangeProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project-new", - ProjectRoleAssertion: false, - ProjectRoleCheck: false, - HasProjectCheck: false, - PrivateLabelingSetting: domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, + Name: gu.Ptr("project-new"), + ProjectRoleAssertion: gu.Ptr(false), + ProjectRoleCheck: gu.Ptr(false), + HasProjectCheck: gu.Ptr(false), + PrivateLabelingSetting: gu.Ptr(domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy), }, resourceOwner: "org1", }, res: res{ - want: &domain.Project{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - ResourceOwner: "org1", - }, - Name: "project-new", - ProjectRoleAssertion: false, - ProjectRoleCheck: false, - HasProjectCheck: false, - PrivateLabelingSetting: domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, { name: "project change without name and unique constraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -429,32 +473,25 @@ func TestCommandSide_ChangeProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project", - ProjectRoleAssertion: false, - ProjectRoleCheck: false, - HasProjectCheck: false, - PrivateLabelingSetting: domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, + Name: gu.Ptr("project"), + ProjectRoleAssertion: gu.Ptr(false), + ProjectRoleCheck: gu.Ptr(false), + HasProjectCheck: gu.Ptr(false), + PrivateLabelingSetting: gu.Ptr(domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy), }, resourceOwner: "org1", }, res: res{ - want: &domain.Project{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - ResourceOwner: "org1", - }, - Name: "project", - ProjectRoleAssertion: false, - ProjectRoleCheck: false, - HasProjectCheck: false, - PrivateLabelingSetting: domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -462,9 +499,10 @@ func TestCommandSide_ChangeProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProject(tt.args.ctx, tt.args.project, tt.args.resourceOwner) + got, err := r.ChangeProject(tt.args.ctx, tt.args.project) if tt.res.err == nil { assert.NoError(t, err) } @@ -472,7 +510,7 @@ func TestCommandSide_ChangeProject(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -480,7 +518,8 @@ func TestCommandSide_ChangeProject(t *testing.T) { func TestCommandSide_DeactivateProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -500,9 +539,8 @@ func TestCommandSide_DeactivateProject(t *testing.T) { { name: "invalid project id, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -513,29 +551,13 @@ func TestCommandSide_DeactivateProject(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, - { - name: "invalid resourceowner, invalid error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - projectID: "project1", - resourceOwner: "", - }, - res: res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -549,8 +571,7 @@ func TestCommandSide_DeactivateProject(t *testing.T) { { name: "project removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -566,6 +587,7 @@ func TestCommandSide_DeactivateProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -579,8 +601,7 @@ func TestCommandSide_DeactivateProject(t *testing.T) { { name: "project already inactive, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -594,6 +615,7 @@ func TestCommandSide_DeactivateProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -605,10 +627,9 @@ func TestCommandSide_DeactivateProject(t *testing.T) { }, }, { - name: "project deactivate, ok", + name: "project deactivate,no resourceOwner, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -622,6 +643,61 @@ func TestCommandSide_DeactivateProject(t *testing.T) { &project.NewAggregate("project1", "org1").Aggregate), ), ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "project deactivate, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "project deactivate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + expectPush( + project.NewProjectDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -638,7 +714,8 @@ func TestCommandSide_DeactivateProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.DeactivateProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) if tt.res.err == nil { @@ -656,7 +733,8 @@ func TestCommandSide_DeactivateProject(t *testing.T) { func TestCommandSide_ReactivateProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -676,9 +754,8 @@ func TestCommandSide_ReactivateProject(t *testing.T) { { name: "invalid project id, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -689,29 +766,13 @@ func TestCommandSide_ReactivateProject(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, - { - name: "invalid resourceowner, invalid error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - projectID: "project1", - resourceOwner: "", - }, - res: res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -725,8 +786,7 @@ func TestCommandSide_ReactivateProject(t *testing.T) { { name: "project removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -742,6 +802,7 @@ func TestCommandSide_ReactivateProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -755,8 +816,7 @@ func TestCommandSide_ReactivateProject(t *testing.T) { { name: "project not inactive, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -766,6 +826,7 @@ func TestCommandSide_ReactivateProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -777,10 +838,9 @@ func TestCommandSide_ReactivateProject(t *testing.T) { }, }, { - name: "project reactivate, ok", + name: "project reactivate, no resourceOwner, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -798,6 +858,69 @@ func TestCommandSide_ReactivateProject(t *testing.T) { &project.NewAggregate("project1", "org1").Aggregate), ), ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "project reactivate, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + eventFromEventPusher( + project.NewProjectDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "project reactivate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + eventFromEventPusher( + project.NewProjectDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + expectPush( + project.NewProjectReactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -814,7 +937,8 @@ func TestCommandSide_ReactivateProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.ReactivateProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) if tt.res.err == nil { @@ -832,7 +956,8 @@ func TestCommandSide_ReactivateProject(t *testing.T) { func TestCommandSide_RemoveProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -852,9 +977,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "invalid project id, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -868,9 +992,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "invalid resourceowner, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -884,10 +1007,10 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -901,8 +1024,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -918,6 +1040,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -931,8 +1054,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project remove, without entityConstraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -950,6 +1072,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { nil), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -965,8 +1088,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project remove, with entityConstraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1003,6 +1125,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1018,8 +1141,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project remove, with multiple entityConstraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1090,6 +1212,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1106,7 +1229,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) if tt.res.err == nil { @@ -1122,6 +1246,328 @@ func TestCommandSide_RemoveProject(t *testing.T) { } } +func TestCommandSide_DeleteProject(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + projectID string + resourceOwner string + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid project id, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, + { + name: "project removed, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + nil), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, { + name: "project remove, no resourceOwner, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + // no saml application events + expectFilter(), + expectPush( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + nil), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + err: nil, + }, + }, + { + name: "project remove, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "project remove, without entityConstraints, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + // no saml application events + expectFilter(), + expectPush( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + nil), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, + { + name: "project remove, with entityConstraints, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + 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", + []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, + "", + ), + ), + ), + expectPush( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + []*eventstore.UniqueConstraint{ + project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata"), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, + { + name: "project remove, with multiple entityConstraints, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + 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://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(), + &project.NewAggregate("project1", "org1").Aggregate, + "app2", + "app", + )), + eventFromEventPusher( + project.NewSAMLConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app2", + "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(), + &project.NewAggregate("project1", "org1").Aggregate, + "app3", + "app", + )), + eventFromEventPusher( + project.NewSAMLConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app3", + "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, + "", + ), + ), + ), + expectPush( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + []*eventstore.UniqueConstraint{ + project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test1.com/saml/metadata"), + project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata"), + project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test3.com/saml/metadata"), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + _, err := r.DeleteProject(tt.args.ctx, tt.args.projectID, 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) + } + }) + } +} + func newProjectChangedEvent(ctx context.Context, projectID, resourceOwner, oldName, newName string, roleAssertion, roleCheck, hasProjectCheck bool, privateLabelingSetting domain.PrivateLabelingSetting) *project.ProjectChangeEvent { changes := []project.ProjectChanges{ project.ChangeProjectRoleAssertion(roleAssertion), @@ -1132,12 +1578,11 @@ func newProjectChangedEvent(ctx context.Context, projectID, resourceOwner, oldNa if newName != "" { changes = append(changes, project.ChangeName(newName)) } - event, _ := project.NewProjectChangeEvent(ctx, + return project.NewProjectChangeEvent(ctx, &project.NewAggregate(projectID, resourceOwner).Aggregate, oldName, changes, ) - return event } func TestAddProject(t *testing.T) { diff --git a/internal/command/resource_ower_model.go b/internal/command/resource_owner_model.go similarity index 100% rename from internal/command/resource_ower_model.go rename to internal/command/resource_owner_model.go diff --git a/internal/command/session.go b/internal/command/session.go index d00e541e62..3c06c22967 100644 --- a/internal/command/session.go +++ b/internal/command/session.go @@ -17,6 +17,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/notification/senders" + "github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/repository/session" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -32,31 +33,33 @@ type SessionCommands struct { eventstore *eventstore.Eventstore eventCommands []eventstore.Command - hasher *crypto.Hasher - intentAlg crypto.EncryptionAlgorithm - totpAlg crypto.EncryptionAlgorithm - otpAlg crypto.EncryptionAlgorithm - createCode encryptedCodeWithDefaultFunc - createPhoneCode encryptedCodeGeneratorWithDefaultFunc - createToken func(sessionID string) (id string, token string, err error) - getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) - now func() time.Time + hasher *crypto.Hasher + intentAlg crypto.EncryptionAlgorithm + totpAlg crypto.EncryptionAlgorithm + otpAlg crypto.EncryptionAlgorithm + createCode encryptedCodeWithDefaultFunc + createPhoneCode encryptedCodeGeneratorWithDefaultFunc + createToken func(sessionID string) (id string, token string, err error) + getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) + now func() time.Time + maxIdPIntentLifetime time.Duration } func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands { return &SessionCommands{ - sessionCommands: cmds, - sessionWriteModel: session, - eventstore: c.eventstore, - hasher: c.userPasswordHasher, - intentAlg: c.idpConfigEncryption, - totpAlg: c.multifactors.OTP.CryptoMFA, - otpAlg: c.userEncryption, - createCode: c.newEncryptedCodeWithDefault, - createPhoneCode: c.newPhoneCode, - createToken: c.sessionTokenCreator, - getCodeVerifier: c.phoneCodeVerifierFromConfig, - now: time.Now, + sessionCommands: cmds, + sessionWriteModel: session, + eventstore: c.eventstore, + hasher: c.userPasswordHasher, + intentAlg: c.idpConfigEncryption, + totpAlg: c.multifactors.OTP.CryptoMFA, + otpAlg: c.userEncryption, + createCode: c.newEncryptedCodeWithDefault, + createPhoneCode: c.newPhoneCode, + createToken: c.sessionTokenCreator, + getCodeVerifier: c.phoneCodeVerifierFromConfig, + now: time.Now, + maxIdPIntentLifetime: c.maxIdPIntentLifetime, } } @@ -92,7 +95,7 @@ func CheckIntent(intentID, token string) SessionCommand { if err := crypto.CheckToken(cmd.intentAlg, token, intentID); err != nil { return nil, err } - cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "") + cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "", cmd.maxIdPIntentLifetime) err := cmd.eventstore.FilterToQueryReducer(ctx, cmd.intentWriteModel) if err != nil { return nil, err @@ -100,6 +103,9 @@ func CheckIntent(intentID, token string) SessionCommand { if cmd.intentWriteModel.State != domain.IDPIntentStateSucceeded { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded") } + if time.Now().After(cmd.intentWriteModel.ExpiresAt()) { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SAf42", "Errors.Intent.Expired") + } if cmd.intentWriteModel.UserID != "" { if cmd.intentWriteModel.UserID != cmd.sessionWriteModel.UserID { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser") @@ -168,6 +174,7 @@ func (s *SessionCommands) PasswordChecked(ctx context.Context, checkedAt time.Ti func (s *SessionCommands) IntentChecked(ctx context.Context, checkedAt time.Time) { s.eventCommands = append(s.eventCommands, session.NewIntentCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt)) + s.eventCommands = append(s.eventCommands, idpintent.NewConsumedEvent(ctx, IDPIntentAggregateFromWriteModel(&s.intentWriteModel.WriteModel))) } func (s *SessionCommands) WebAuthNChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement, rpid string) { diff --git a/internal/command/session_test.go b/internal/command/session_test.go index 60027d3a05..e65f32fb57 100644 --- a/internal/command/session_test.go +++ b/internal/command/session_test.go @@ -695,6 +695,7 @@ func TestCommands_updateSession(t *testing.T) { "userID2", nil, "", + time.Now().Add(time.Hour), ), ), ), @@ -757,6 +758,111 @@ func TestCommands_updateSession(t *testing.T) { err: zerrors.ThrowPermissionDenied(nil, "CRYPTO-CRYPTO", "Errors.Intent.InvalidToken"), }, }, + { + "set user, intent token already consumed", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, + "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false), + ), + eventFromEventPusher( + idpintent.NewSucceededEvent(context.Background(), + &idpintent.NewAggregate("intent", "instance1").Aggregate, + nil, + "idpUserID", + "idpUsername", + "userID", + nil, + "", + time.Now().Add(time.Hour), + ), + ), + eventFromEventPusher( + idpintent.NewConsumedEvent(context.Background(), + &idpintent.NewAggregate("intent", "instance1").Aggregate, + ), + ), + ), + ), + }, + args{ + ctx: authz.NewMockContext("instance1", "", ""), + checks: &SessionCommands{ + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), + sessionCommands: []SessionCommand{ + CheckUser("userID", "org1", &language.Afrikaans), + CheckIntent("intent", "aW50ZW50"), + }, + createToken: func(sessionID string) (string, string, error) { + return "tokenID", + "token", + nil + }, + intentAlg: decryption(nil), + now: func() time.Time { + return testNow + }, + }, + metadata: map[string][]byte{ + "key": []byte("value"), + }, + }, + res{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded"), + }, + }, + { + "set user, intent token already expired", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, + "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false), + ), + eventFromEventPusher( + idpintent.NewSucceededEvent(context.Background(), + &idpintent.NewAggregate("intent", "instance1").Aggregate, + nil, + "idpUserID", + "idpUsername", + "userID", + nil, + "", + time.Now().Add(-time.Hour), + ), + ), + ), + ), + }, + args{ + ctx: authz.NewMockContext("instance1", "", ""), + checks: &SessionCommands{ + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), + sessionCommands: []SessionCommand{ + CheckUser("userID", "org1", &language.Afrikaans), + CheckIntent("intent", "aW50ZW50"), + }, + createToken: func(sessionID string) (string, string, error) { + return "tokenID", + "token", + nil + }, + intentAlg: decryption(nil), + now: func() time.Time { + return testNow + }, + }, + metadata: map[string][]byte{ + "key": []byte("value"), + }, + }, + res{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SAf42", "Errors.Intent.Expired"), + }, + }, { "set user, intent, metadata and token", fields{ @@ -768,13 +874,14 @@ func TestCommands_updateSession(t *testing.T) { ), eventFromEventPusher( idpintent.NewSucceededEvent(context.Background(), - &idpintent.NewAggregate("id", "instance1").Aggregate, + &idpintent.NewAggregate("intent", "instance1").Aggregate, nil, "idpUserID", "idpUsername", "userID", nil, "", + time.Now().Add(time.Hour), ), ), ), @@ -783,6 +890,7 @@ func TestCommands_updateSession(t *testing.T) { "userID", "org1", testNow, &language.Afrikaans), session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, testNow), + idpintent.NewConsumedEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate), session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, map[string][]byte{"key": []byte("value")}), session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, @@ -842,13 +950,14 @@ func TestCommands_updateSession(t *testing.T) { ), eventFromEventPusher( idpintent.NewSucceededEvent(context.Background(), - &idpintent.NewAggregate("id", "instance1").Aggregate, + &idpintent.NewAggregate("intent", "instance1").Aggregate, nil, "idpUserID", "idpUsername", "", nil, "", + time.Now().Add(time.Hour), ), ), ), @@ -866,6 +975,7 @@ func TestCommands_updateSession(t *testing.T) { "userID", "org1", testNow, &language.Afrikaans), session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, testNow), + idpintent.NewConsumedEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate), session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID"), ), diff --git a/internal/command/user.go b/internal/command/user.go index 6b65aa83ec..0db4fda328 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -353,21 +353,27 @@ func (c *Commands) userWriteModelByID(ctx context.Context, userID, resourceOwner return writeModel, nil } -func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string) (exists bool, err error) { +func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string, machineOnly bool) (exists bool, err error) { + eventTypes := []eventstore.EventType{ + user.MachineAddedEventType, + user.UserRemovedType, + } + if !machineOnly { + eventTypes = append(eventTypes, + user.HumanRegisteredType, + user.UserV1RegisteredType, + user.HumanAddedType, + user.UserV1AddedType, + ) + } events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(resourceOwner). OrderAsc(). AddQuery(). AggregateTypes(user.AggregateType). AggregateIDs(id). - EventTypes( - user.HumanRegisteredType, - user.UserV1RegisteredType, - user.HumanAddedType, - user.UserV1AddedType, - user.MachineAddedEventType, - user.UserRemovedType, - ).Builder()) + EventTypes(eventTypes...). + Builder()) if err != nil { return false, err } diff --git a/internal/command/user_human.go b/internal/command/user_human.go index 9e6ba43629..07628b9e19 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -428,7 +428,7 @@ func (h *AddHuman) shouldAddInitCode() bool { } // Deprecated: use commands.AddUserHuman -func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) { +func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, state *domain.UserState, links []*domain.UserIDPLink, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -455,10 +455,32 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. } } - events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, links, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) + events, userAgg, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, links, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) if err != nil { return nil, nil, err } + if state != nil { + var event eventstore.Command + switch *state { + case domain.UserStateInactive: + event = user.NewUserDeactivatedEvent(ctx, userAgg) + case domain.UserStateLocked: + event = user.NewUserLockedEvent(ctx, userAgg) + case domain.UserStateDeleted: + // users are never imported if deleted + case domain.UserStateActive: + // added because of the linter + case domain.UserStateSuspend: + // added because of the linter + case domain.UserStateInitial: + // added because of the linter + case domain.UserStateUnspecified: + // added because of the linter + } + if event != nil { + events = append(events, event) + } + } pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, nil, err @@ -479,48 +501,48 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. return writeModelToHuman(addedHuman), passwordlessCode, nil } -func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { +func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() if orgID == "" { - return nil, nil, nil, "", zerrors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.Org.Empty") + return nil, nil, nil, nil, "", zerrors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.Org.Empty") } if err = human.Normalize(); err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, "", err } - events, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) + events, userAgg, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, "", err } if passwordless { var codeEvent eventstore.Command codeEvent, passwordlessCodeWriteModel, code, err = c.humanAddPasswordlessInitCode(ctx, human.AggregateID, orgID, true, passwordlessCodeGenerator) if err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, "", err } events = append(events, codeEvent) } - return events, humanWriteModel, passwordlessCodeWriteModel, code, nil + return events, userAgg, humanWriteModel, passwordlessCodeWriteModel, code, nil } -func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, addedHuman *HumanWriteModel, err error) { +func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, addedHuman *HumanWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() if err = human.CheckDomainPolicy(domainPolicy); err != nil { - return nil, nil, err + return nil, nil, nil, err } human.Username = strings.TrimSpace(human.Username) human.EmailAddress = human.EmailAddress.Normalize() if err = c.userValidateDomain(ctx, orgID, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil { - return nil, nil, err + return nil, nil, nil, err } if human.AggregateID == "" { userID, err := c.idGenerator.Next() if err != nil { - return nil, nil, err + return nil, nil, nil, err } human.AggregateID = userID } @@ -528,20 +550,21 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. human.EnsureDisplayName() if human.Password != nil { if err := human.HashPasswordIfExisting(ctx, pwPolicy, c.userPasswordHasher, human.Password.ChangeRequired); err != nil { - return nil, nil, err + return nil, nil, nil, err } } addedHuman = NewHumanWriteModel(human.AggregateID, orgID) - //TODO: adlerhurst maybe we could simplify the code below - userAgg := UserAggregateFromWriteModel(&addedHuman.WriteModel) + + // TODO: adlerhurst maybe we could simplify the code below + userAgg = UserAggregateFromWriteModelCtx(ctx, &addedHuman.WriteModel) events = append(events, createAddHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain)) for _, link := range links { event, err := c.addUserIDPLink(ctx, userAgg, link, false) if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, event) } @@ -549,7 +572,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. if human.IsInitialState(passwordless, len(links) > 0) { initCode, err := domain.NewInitUserCode(initCodeGenerator) if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, user.NewHumanInitialCodeAddedEvent(ctx, userAgg, initCode.Code, initCode.Expiry, "")) } else { @@ -558,7 +581,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. } else { emailCode, _, err := domain.NewEmailCode(emailCodeGenerator) if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, "")) } @@ -567,14 +590,14 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. if human.Phone != nil && human.PhoneNumber != "" && !human.IsPhoneVerified { phoneCode, generatorID, err := c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.CryptedCode(), phoneCode.CodeExpiry(), generatorID)) } else if human.Phone != nil && human.PhoneNumber != "" && human.IsPhoneVerified { events = append(events, user.NewHumanPhoneVerifiedEvent(ctx, userAgg)) } - return events, addedHuman, nil + return events, userAgg, addedHuman, nil } func (c *Commands) HumanSkipMFAInit(ctx context.Context, userID, resourceowner string) (err error) { diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 78d7248516..1ef3e2aab6 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -1200,7 +1200,8 @@ func TestCommandSide_AddHuman(t *testing.T) { }, wantID: "user1", }, - }, { + }, + { name: "add human (with return code), ok", fields: fields{ eventstore: expectEventstore( @@ -1432,6 +1433,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { orgID string human *domain.Human passwordless bool + state *domain.UserState links []*domain.UserIDPLink secretGenerator crypto.Generator passwordlessInitCode crypto.Generator @@ -1584,7 +1586,8 @@ func TestCommandSide_ImportHuman(t *testing.T) { res: res{ err: zerrors.IsErrorInvalidArgument, }, - }, { + }, + { name: "add human (with password and initial code), ok", given: func(t *testing.T) (fields, args) { return fields{ @@ -2985,6 +2988,364 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, }, + { + name: "add human (with idp, auto creation not allowed) + deactivated state, ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{ + IsCreationAllowed: gu.Ptr(true), + IsAutoCreation: gu.Ptr(false), + }), + }, + ) + return e + }(), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectPush( + newAddHumanEvent("", false, true, "", AllowedLanguage), + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "idpID", + "name", + "externalID", + ), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + state: func() *domain.UserState { + state := domain.UserStateInactive + return &state + }(), + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + State: domain.UserStateInactive, + }, + }, + }, + { + name: "add human (with idp, auto creation not allowed) + locked state, ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{ + IsCreationAllowed: gu.Ptr(true), + IsAutoCreation: gu.Ptr(false), + }), + }, + ) + return e + }(), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectPush( + newAddHumanEvent("", false, true, "", AllowedLanguage), + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "idpID", + "name", + "externalID", + ), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + user.NewUserLockedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + state: func() *domain.UserState { + state := domain.UserStateLocked + return &state + }(), + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + State: domain.UserStateLocked, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2996,7 +3357,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { newEncryptedCodeWithDefault: f.newEncryptedCodeWithDefault, defaultSecretGenerators: f.defaultSecretGenerators, } - gotHuman, gotCode, err := r.ImportHuman(a.ctx, a.orgID, a.human, a.passwordless, a.links, a.secretGenerator, a.secretGenerator, a.secretGenerator, a.secretGenerator) + gotHuman, gotCode, err := r.ImportHuman(a.ctx, a.orgID, a.human, a.passwordless, a.state, a.links, a.secretGenerator, a.secretGenerator, a.secretGenerator, a.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_machine.go b/internal/command/user_machine.go index 1ec32450ac..75ed43ee69 100644 --- a/internal/command/user_machine.go +++ b/internal/command/user_machine.go @@ -25,6 +25,7 @@ type Machine struct { Name string Description string AccessTokenType domain.OIDCTokenType + PermissionCheck PermissionCheck } func (m *Machine) IsZero() bool { @@ -33,8 +34,8 @@ func (m *Machine) IsZero() bool { func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown2", "Errors.ResourceOwnerMissing") + if a.ResourceOwner == "" && machine.PermissionCheck == nil { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown3", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-p0p2mi", "Errors.User.UserIDMissing") @@ -49,7 +50,7 @@ func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validati ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, machine.PermissionCheck) if err != nil { return nil, err } @@ -67,7 +68,18 @@ func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validati } } -func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain.ObjectDetails, err error) { +type addMachineOption func(context.Context, *Machine) error + +func AddMachineWithUsernameToIDFallback() addMachineOption { + return func(ctx context.Context, m *Machine) error { + if m.Username == "" { + m.Username = m.AggregateID + } + return nil + } +} + +func (c *Commands) AddMachine(ctx context.Context, machine *Machine, state *domain.UserState, check PermissionCheck, options ...addMachineOption) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -80,11 +92,44 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain. } agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner) + for _, option := range options { + if err = option(ctx, machine); err != nil { + return nil, err + } + } + if check != nil { + if err = check(machine.ResourceOwner, machine.AggregateID); err != nil { + return nil, err + } + } cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddMachineCommand(agg, machine)) if err != nil { return nil, err } + if state != nil { + var cmd eventstore.Command + switch *state { + case domain.UserStateInactive: + cmd = user.NewUserDeactivatedEvent(ctx, &agg.Aggregate) + case domain.UserStateLocked: + cmd = user.NewUserLockedEvent(ctx, &agg.Aggregate) + case domain.UserStateDeleted: + // users are never imported if deleted + case domain.UserStateActive: + // added because of the linter + case domain.UserStateSuspend: + // added because of the linter + case domain.UserStateInitial: + // added because of the linter + case domain.UserStateUnspecified: + // added because of the linter + } + if cmd != nil { + cmds = append(cmds, cmd) + } + } + events, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err @@ -97,6 +142,7 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain. }, nil } +// Deprecated: use ChangeUserMachine instead func (c *Commands) ChangeMachine(ctx context.Context, machine *Machine) (*domain.ObjectDetails, error) { agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, changeMachineCommand(agg, machine)) @@ -118,24 +164,21 @@ func (c *Commands) ChangeMachine(ctx context.Context, machine *Machine) (*domain func changeMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { + if a.ResourceOwner == "" && machine.PermissionCheck == nil { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown3", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-p0p3mi", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, machine.PermissionCheck) if err != nil { return nil, err } if !isUserStateExists(writeModel.UserState) { return nil, zerrors.ThrowNotFound(nil, "COMMAND-5M0od", "Errors.User.NotFound") } - changedEvent, hasChanged, err := writeModel.NewChangedEvent(ctx, &a.Aggregate, machine.Name, machine.Description, machine.AccessTokenType) - if err != nil { - return nil, err - } + changedEvent, hasChanged := writeModel.NewChangedEvent(ctx, &a.Aggregate, machine.Name, machine.Description, machine.AccessTokenType) if !hasChanged { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2n8vs", "Errors.User.NotChanged") } @@ -147,10 +190,9 @@ func changeMachineCommand(a *user.Aggregate, machine *Machine) preparation.Valid } } -func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, filter preparation.FilterToQueryReducer) (_ *MachineWriteModel, err error) { +func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, filter preparation.FilterToQueryReducer, permissionCheck PermissionCheck) (_ *MachineWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewMachineWriteModel(userID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { @@ -161,5 +203,10 @@ func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, fil } writeModel.AppendEvents(events...) err = writeModel.Reduce() + if permissionCheck != nil { + if err := permissionCheck(writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + } return writeModel, err } diff --git a/internal/command/user_machine_key.go b/internal/command/user_machine_key.go index 8a0f0f437b..d628bf4c2d 100644 --- a/internal/command/user_machine_key.go +++ b/internal/command/user_machine_key.go @@ -15,12 +15,14 @@ import ( ) type AddMachineKey struct { - Type domain.AuthNKeyType - ExpirationDate time.Time + Type domain.AuthNKeyType + ExpirationDate time.Time + PermissionCheck PermissionCheck } type MachineKey struct { models.ObjectRoot + PermissionCheck PermissionCheck KeyID string Type domain.AuthNKeyType @@ -64,7 +66,7 @@ func (key *MachineKey) Detail() ([]byte, error) { } func (key *MachineKey) content() error { - if key.ResourceOwner == "" { + if key.PermissionCheck == nil && key.ResourceOwner == "" { return zerrors.ThrowInvalidArgument(nil, "COMMAND-kqpoix", "Errors.ResourceOwnerMissing") } if key.AggregateID == "" { @@ -91,7 +93,7 @@ func (key *MachineKey) valid() (err error) { } func (key *MachineKey) checkAggregate(ctx context.Context, filter preparation.FilterToQueryReducer) error { - if exists, err := ExistsUser(ctx, filter, key.AggregateID, key.ResourceOwner); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, key.AggregateID, key.ResourceOwner, true); err != nil || !exists { return zerrors.ThrowPreconditionFailed(err, "COMMAND-bnipwm1", "Errors.User.NotFound") } return nil @@ -142,7 +144,7 @@ func prepareAddUserMachineKey(machineKey *MachineKey, keySize int) preparation.V return nil, err } } - writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner) + writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner, machineKey.PermissionCheck) if err != nil { return nil, err } @@ -186,7 +188,7 @@ func prepareRemoveUserMachineKey(machineKey *MachineKey) preparation.Validation return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner) + writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner, machineKey.PermissionCheck) if err != nil { return nil, err } @@ -204,16 +206,18 @@ func prepareRemoveUserMachineKey(machineKey *MachineKey) preparation.Validation } } -func getMachineKeyWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, keyID, resourceOwner string) (_ *MachineKeyWriteModel, err error) { +func getMachineKeyWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, keyID, resourceOwner string, permissionCheck PermissionCheck) (_ *MachineKeyWriteModel, err error) { writeModel := NewMachineKeyWriteModel(userID, keyID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err } - if len(events) == 0 { - return writeModel, nil - } writeModel.AppendEvents(events...) err = writeModel.Reduce() + if permissionCheck != nil { + if err := permissionCheck(writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + } return writeModel, err } diff --git a/internal/command/user_machine_model.go b/internal/command/user_machine_model.go index b7dfb02d32..1ed6c8ca58 100644 --- a/internal/command/user_machine_model.go +++ b/internal/command/user_machine_model.go @@ -2,7 +2,6 @@ package command import ( "context" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -106,9 +105,8 @@ func (wm *MachineWriteModel) NewChangedEvent( name, description string, accessTokenType domain.OIDCTokenType, -) (*user.MachineChangedEvent, bool, error) { +) (*user.MachineChangedEvent, bool) { changes := make([]user.MachineChanges, 0) - var err error if wm.Name != name { changes = append(changes, user.ChangeName(name)) @@ -120,11 +118,8 @@ func (wm *MachineWriteModel) NewChangedEvent( changes = append(changes, user.ChangeAccessTokenType(accessTokenType)) } if len(changes) == 0 { - return nil, false, nil + return nil, false } - changeEvent, err := user.NewMachineChangedEvent(ctx, aggregate, changes) - if err != nil { - return nil, false, err - } - return changeEvent, true, nil + changeEvent := user.NewMachineChangedEvent(ctx, aggregate, changes) + return changeEvent, true } diff --git a/internal/command/user_machine_secret.go b/internal/command/user_machine_secret.go index 3349fc90a5..34e9c0c5cc 100644 --- a/internal/command/user_machine_secret.go +++ b/internal/command/user_machine_secret.go @@ -11,7 +11,8 @@ import ( ) type GenerateMachineSecret struct { - ClientSecret string + PermissionCheck PermissionCheck + ClientSecret string } func (c *Commands) GenerateMachineSecret(ctx context.Context, userID string, resourceOwner string, set *GenerateMachineSecret) (*domain.ObjectDetails, error) { @@ -35,14 +36,14 @@ func (c *Commands) GenerateMachineSecret(ctx context.Context, userID string, res func (c *Commands) prepareGenerateMachineSecret(a *user.Aggregate, set *GenerateMachineSecret) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x0992n", "Errors.ResourceOwnerMissing") + if a.ResourceOwner == "" && set.PermissionCheck == nil { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-bzoqjs", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, set.PermissionCheck) if err != nil { return nil, err } @@ -62,9 +63,10 @@ func (c *Commands) prepareGenerateMachineSecret(a *user.Aggregate, set *Generate } } -func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resourceOwner string) (*domain.ObjectDetails, error) { +func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resourceOwner string, permissionCheck PermissionCheck) (*domain.ObjectDetails, error) { agg := user.NewAggregate(userID, resourceOwner) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveMachineSecret(agg)) + //nolint:staticcheck + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveMachineSecret(agg, permissionCheck)) if err != nil { return nil, err } @@ -81,16 +83,16 @@ func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resou }, nil } -func prepareRemoveMachineSecret(a *user.Aggregate) preparation.Validation { +func prepareRemoveMachineSecret(a *user.Aggregate, check PermissionCheck) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing") + if a.ResourceOwner == "" && check == nil { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x0992n", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-bzosjs", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, check) if err != nil { return nil, err } diff --git a/internal/command/user_machine_secret_test.go b/internal/command/user_machine_secret_test.go index 4c6d16960c..8e839efe07 100644 --- a/internal/command/user_machine_secret_test.go +++ b/internal/command/user_machine_secret_test.go @@ -44,7 +44,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ctx: context.Background(), userID: "", resourceOwner: "org1", - set: nil, + set: new(GenerateMachineSecret), }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -59,7 +59,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ctx: context.Background(), userID: "user1", resourceOwner: "", - set: nil, + set: new(GenerateMachineSecret), }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -76,7 +76,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ctx: context.Background(), userID: "user1", resourceOwner: "org1", - set: nil, + set: new(GenerateMachineSecret), }, res: res{ err: zerrors.IsPreconditionFailed, @@ -289,7 +289,7 @@ func TestCommandSide_RemoveMachineSecret(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, } - got, err := r.RemoveMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) + got, err := r.RemoveMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, nil) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_machine_test.go b/internal/command/user_machine_test.go index c7b4b8caf4..6d94154a42 100644 --- a/internal/command/user_machine_test.go +++ b/internal/command/user_machine_test.go @@ -24,6 +24,9 @@ func TestCommandSide_AddMachine(t *testing.T) { type args struct { ctx context.Context machine *Machine + state *domain.UserState + check PermissionCheck + options func(*Commands) []addMachineOption } type res struct { want *domain.ObjectDetails @@ -194,14 +197,348 @@ func TestCommandSide_AddMachine(t *testing.T) { }, }, }, + { + name: "with username fallback to given username", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "aggregateID"), + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + "username", + "name", + "", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Name: "name", + Username: "username", + }, + options: func(commands *Commands) []addMachineOption { + return []addMachineOption{ + AddMachineWithUsernameToIDFallback(), + } + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with username fallback to generated id", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "aggregateID"), + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + "aggregateID", + "name", + "", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Name: "name", + }, + options: func(commands *Commands) []addMachineOption { + return []addMachineOption{ + AddMachineWithUsernameToIDFallback(), + } + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with username fallback to given id", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + "aggregateID", + "name", + "", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + AggregateID: "aggregateID", + }, + Name: "name", + }, + options: func(commands *Commands) []addMachineOption { + return []addMachineOption{ + AddMachineWithUsernameToIDFallback(), + } + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with succeeding permission check, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + check: func(resourceOwner, aggregateID string) error { + return nil + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with failing permission check, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + check: func(resourceOwner, aggregateID string) error { + return zerrors.ThrowPermissionDenied(nil, "", "") + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "add machine, ok + deactive state", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + state: func() *domain.UserState { + state := domain.UserStateInactive + return &state + }(), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "add machine, ok + locked state", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + user.NewUserLockedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + state: func() *domain.UserState { + state := domain.UserStateLocked + return &state + }(), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + checkPermission: newMockPermissionCheckAllowed(), } - got, err := r.AddMachine(tt.args.ctx, tt.args.machine) + var options []addMachineOption + if tt.args.options != nil { + options = tt.args.options(r) + } + got, err := r.AddMachine(tt.args.ctx, tt.args.machine, tt.args.state, tt.args.check, options...) if tt.res.err == nil { assert.NoError(t, err) } @@ -391,7 +728,7 @@ func TestCommandSide_ChangeMachine(t *testing.T) { } func newMachineChangedEvent(ctx context.Context, userID, resourceOwner, name, description string) *user.MachineChangedEvent { - event, _ := user.NewMachineChangedEvent(ctx, + event := user.NewMachineChangedEvent(ctx, &user.NewAggregate(userID, resourceOwner).Aggregate, []user.MachineChanges{ user.ChangeName(name), diff --git a/internal/command/user_personal_access_token.go b/internal/command/user_personal_access_token.go index 0faf85d5eb..f37953f3d6 100644 --- a/internal/command/user_personal_access_token.go +++ b/internal/command/user_personal_access_token.go @@ -21,6 +21,7 @@ type AddPat struct { type PersonalAccessToken struct { models.ObjectRoot + PermissionCheck PermissionCheck ExpirationDate time.Time Scopes []string @@ -43,7 +44,7 @@ func NewPersonalAccessToken(resourceOwner string, userID string, expirationDate } func (pat *PersonalAccessToken) content() error { - if pat.ResourceOwner == "" { + if pat.ResourceOwner == "" && pat.PermissionCheck == nil { return zerrors.ThrowInvalidArgument(nil, "COMMAND-xs0k2n", "Errors.ResourceOwnerMissing") } if pat.AggregateID == "" { @@ -109,11 +110,10 @@ func prepareAddPersonalAccessToken(pat *PersonalAccessToken, algorithm crypto.En if err := pat.checkAggregate(ctx, filter); err != nil { return nil, err } - writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner) + writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner, pat.PermissionCheck) if err != nil { return nil, err } - pat.Token, err = createToken(algorithm, writeModel.TokenID, writeModel.AggregateID) if err != nil { return nil, err @@ -155,7 +155,7 @@ func prepareRemovePersonalAccessToken(pat *PersonalAccessToken) preparation.Vali return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) { - writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner) + writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner, pat.PermissionCheck) if err != nil { return nil, err } @@ -181,16 +181,18 @@ func createToken(algorithm crypto.EncryptionAlgorithm, tokenID, userID string) ( return base64.RawURLEncoding.EncodeToString(encrypted), nil } -func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string) (_ *PersonalAccessTokenWriteModel, err error) { +func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string, check PermissionCheck) (_ *PersonalAccessTokenWriteModel, err error) { writeModel := NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err } - if len(events) == 0 { - return writeModel, nil - } writeModel.AppendEvents(events...) - err = writeModel.Reduce() + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if check != nil { + err = check(writeModel.ResourceOwner, writeModel.AggregateID) + } return writeModel, err } diff --git a/internal/command/user_test.go b/internal/command/user_test.go index 9abae187c1..6a1597fc8b 100644 --- a/internal/command/user_test.go +++ b/internal/command/user_test.go @@ -1813,7 +1813,7 @@ func TestExistsUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner) + gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner, false) if (err != nil) != tt.wantErr { t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/internal/command/user_v2.go b/internal/command/user_v2.go index 00ca85aaf4..be10fd03fe 100644 --- a/internal/command/user_v2.go +++ b/internal/command/user_v2.go @@ -5,7 +5,6 @@ import ( "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/user" @@ -117,36 +116,6 @@ func (c *Commands) ReactivateUserV2(ctx context.Context, userID string) (*domain return writeModelToObjectDetails(&existingHuman.WriteModel), nil } -func (c *Commands) checkPermissionUpdateUser(ctx context.Context, resourceOwner, userID string) error { - if userID != "" && userID == authz.GetCtxData(ctx).UserID { - return nil - } - if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil { - return err - } - return nil -} - -func (c *Commands) checkPermissionUpdateUserCredentials(ctx context.Context, resourceOwner, userID string) error { - if userID != "" && userID == authz.GetCtxData(ctx).UserID { - return nil - } - if err := c.checkPermission(ctx, domain.PermissionUserCredentialWrite, resourceOwner, userID); err != nil { - return err - } - return nil -} - -func (c *Commands) checkPermissionDeleteUser(ctx context.Context, resourceOwner, userID string) error { - if userID != "" && userID == authz.GetCtxData(ctx).UserID { - return nil - } - if err := c.checkPermission(ctx, domain.PermissionUserDelete, resourceOwner, userID); err != nil { - return err - } - return nil -} - func (c *Commands) userStateWriteModel(ctx context.Context, userID string) (writeModel *UserV2WriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -163,7 +132,6 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin if userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-vaipl7s13l", "Errors.User.UserIDMissing") } - existingUser, err := c.userRemoveWriteModel(ctx, userID, resourceOwner) if err != nil { return nil, err @@ -174,7 +142,6 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin if err := c.checkPermissionDeleteUser(ctx, existingUser.ResourceOwner, existingUser.AggregateID); err != nil { return nil, err } - domainPolicy, err := c.domainPolicyWriteModel(ctx, existingUser.ResourceOwner) if err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-l40ykb3xh2", "Errors.Org.DomainPolicy.NotExisting") diff --git a/internal/command/user_v2_human.go b/internal/command/user_v2_human.go index f88e2017d5..0945ae7578 100644 --- a/internal/command/user_v2_human.go +++ b/internal/command/user_v2_human.go @@ -5,6 +5,7 @@ import ( "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -121,7 +122,10 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if resourceOwner == "" { return zerrors.ThrowInvalidArgument(nil, "COMMA-095xh8fll1", "Errors.Internal") } - + if human.Details == nil { + human.Details = &domain.ObjectDetails{} + } + human.Details.ResourceOwner = resourceOwner if err := human.Validate(c.userPasswordHasher); err != nil { return err } @@ -132,7 +136,12 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human return err } } - + // check for permission to create user on resourceOwner + if !human.Register { + if err := c.checkPermissionUpdateUser(ctx, resourceOwner, human.ID); err != nil { + return err + } + } // only check if user is already existing existingHuman, err := c.userExistsWriteModel( ctx, @@ -144,12 +153,6 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if isUserStateExists(existingHuman.UserState) { return zerrors.ThrowPreconditionFailed(nil, "COMMAND-7yiox1isql", "Errors.User.AlreadyExisting") } - // check for permission to create user on resourceOwner - if !human.Register { - if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, human.ID); err != nil { - return err - } - } // add resourceowner for the events with the aggregate existingHuman.ResourceOwner = resourceOwner @@ -161,6 +164,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if err = c.userValidateDomain(ctx, resourceOwner, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil { return err } + var createCmd humanCreationCommand if human.Register { createCmd = user.NewHumanRegisteredEvent( @@ -203,17 +207,33 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human return err } - cmds := make([]eventstore.Command, 0, 3) - cmds = append(cmds, createCmd) - - cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, existingHuman.Aggregate(), human, alg, allowInitMail) + cmds, err := c.addUserHumanCommands(ctx, filter, existingHuman, human, allowInitMail, alg, createCmd) if err != nil { return err } + if len(cmds) == 0 { + human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) + return nil + } + err = c.pushAppendAndReduce(ctx, existingHuman, cmds...) + if err != nil { + return err + } + human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) + return nil +} + +func (c *Commands) addUserHumanCommands(ctx context.Context, filter preparation.FilterToQueryReducer, existingHuman *UserV2WriteModel, human *AddHuman, allowInitMail bool, alg crypto.EncryptionAlgorithm, addUserCommand eventstore.Command) ([]eventstore.Command, error) { + cmds := []eventstore.Command{addUserCommand} + var err error + cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, existingHuman.Aggregate(), human, alg, allowInitMail) + if err != nil { + return nil, err + } cmds, err = c.addHumanCommandPhone(ctx, filter, cmds, existingHuman.Aggregate(), human, alg) if err != nil { - return err + return nil, err } for _, metadataEntry := range human.Metadata { @@ -227,7 +247,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human for _, link := range human.Links { cmd, err := addLink(ctx, filter, existingHuman.Aggregate(), link) if err != nil { - return err + return nil, err } cmds = append(cmds, cmd) } @@ -235,7 +255,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if human.TOTPSecret != "" { encryptedSecret, err := crypto.Encrypt([]byte(human.TOTPSecret), c.multifactors.OTP.CryptoMFA) if err != nil { - return err + return nil, err } cmds = append(cmds, user.NewHumanOTPAddedEvent(ctx, &existingHuman.Aggregate().Aggregate, encryptedSecret), @@ -246,18 +266,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if human.SetInactive { cmds = append(cmds, user.NewUserDeactivatedEvent(ctx, &existingHuman.Aggregate().Aggregate)) } - - if len(cmds) == 0 { - human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) - return nil - } - - err = c.pushAppendAndReduce(ctx, existingHuman, cmds...) - if err != nil { - return err - } - human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) - return nil + return cmds, nil } func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg crypto.EncryptionAlgorithm) (err error) { @@ -341,7 +350,6 @@ func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg if human.State != nil { // only allow toggling between active and inactive // any other target state is not supported - // the existing human's state has to be the switch { case isUserStateActive(*human.State): if isUserStateActive(existingHuman.UserState) { diff --git a/internal/command/user_v2_human_test.go b/internal/command/user_v2_human_test.go index 2b4399fb2a..e44e182b92 100644 --- a/internal/command/user_v2_human_test.go +++ b/internal/command/user_v2_human_test.go @@ -302,9 +302,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { { name: "add human (with initial code), no permission", fields: fields{ - eventstore: expectEventstore( - expectFilter(), - ), + eventstore: expectEventstore(), checkPermission: newMockPermissionCheckNotAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), @@ -326,9 +324,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, res: res{ - err: func(err error) bool { - return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) - }, + err: zerrors.IsPermissionDenied, }, }, { diff --git a/internal/command/user_v2_invite.go b/internal/command/user_v2_invite.go index 1325d2e0c9..430ba8c7d1 100644 --- a/internal/command/user_v2_invite.go +++ b/internal/command/user_v2_invite.go @@ -19,14 +19,34 @@ type CreateUserInvite struct { URLTemplate string ReturnCode bool ApplicationName string + AuthRequestID string } func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvite) (details *domain.ObjectDetails, returnCode *string, err error) { + return c.sendInviteCode(ctx, invite, "", false) +} + +// ResendInviteCode resends the invite mail with a new code and an optional authRequestID. +// It will reuse the applicationName from the previous code. +func (c *Commands) ResendInviteCode(ctx context.Context, userID, resourceOwner, authRequestID string) (objectDetails *domain.ObjectDetails, err error) { + details, _, err := c.sendInviteCode( + ctx, + &CreateUserInvite{ + UserID: userID, + AuthRequestID: authRequestID, + }, + resourceOwner, + true, + ) + return details, err +} + +func (c *Commands) sendInviteCode(ctx context.Context, invite *CreateUserInvite, resourceOwner string, requireExisting bool) (details *domain.ObjectDetails, returnCode *string, err error) { invite.UserID = strings.TrimSpace(invite.UserID) if invite.UserID == "" { return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4jio3", "Errors.User.UserIDMissing") } - wm, err := c.userInviteCodeWriteModel(ctx, invite.UserID, "") + wm, err := c.userInviteCodeWriteModel(ctx, invite.UserID, resourceOwner) if err != nil { return nil, nil, err } @@ -39,10 +59,22 @@ func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvit if !wm.CreationAllowed() { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-EF34g", "Errors.User.AlreadyInitialised") } + if requireExisting && wm.InviteCode == nil || wm.CodeReturned { + return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound") + } code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint if err != nil { return nil, nil, err } + if invite.URLTemplate == "" { + invite.URLTemplate = wm.URLTemplate + } + if invite.ApplicationName == "" { + invite.ApplicationName = wm.ApplicationName + } + if invite.AuthRequestID == "" { + invite.AuthRequestID = wm.AuthRequestID + } err = c.pushAppendAndReduce(ctx, wm, user.NewHumanInviteCodeAddedEvent( ctx, UserAggregateFromWriteModelCtx(ctx, &wm.WriteModel), @@ -51,7 +83,7 @@ func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvit invite.URLTemplate, invite.ReturnCode, invite.ApplicationName, - "", + invite.AuthRequestID, )) if err != nil { return nil, nil, err @@ -62,53 +94,6 @@ func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvit return writeModelToObjectDetails(&wm.WriteModel), returnCode, nil } -// ResendInviteCode resends the invite mail with a new code and an optional authRequestID. -// It will reuse the applicationName from the previous code. -func (c *Commands) ResendInviteCode(ctx context.Context, userID, resourceOwner, authRequestID string) (objectDetails *domain.ObjectDetails, err error) { - if userID == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing") - } - - existingCode, err := c.userInviteCodeWriteModel(ctx, userID, resourceOwner) - if err != nil { - return nil, err - } - if err := c.checkPermissionUpdateUser(ctx, existingCode.ResourceOwner, userID); err != nil { - return nil, err - } - if !existingCode.UserState.Exists() { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-H3b2a", "Errors.User.NotFound") - } - if !existingCode.CreationAllowed() { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Gg42s", "Errors.User.AlreadyInitialised") - } - if existingCode.InviteCode == nil || existingCode.CodeReturned { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound") - } - code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint - if err != nil { - return nil, err - } - if authRequestID == "" { - authRequestID = existingCode.AuthRequestID - } - err = c.pushAppendAndReduce(ctx, existingCode, - user.NewHumanInviteCodeAddedEvent( - ctx, - UserAggregateFromWriteModelCtx(ctx, &existingCode.WriteModel), - code.Crypted, - code.Expiry, - existingCode.URLTemplate, - false, - existingCode.ApplicationName, - authRequestID, - )) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&existingCode.WriteModel), nil -} - func (c *Commands) InviteCodeSent(ctx context.Context, userID, orgID string) (err error) { if userID == "" { return zerrors.ThrowInvalidArgument(nil, "COMMAND-Sgf31", "Errors.User.UserIDMissing") diff --git a/internal/command/user_v2_invite_model.go b/internal/command/user_v2_invite_model.go index 23f6322a19..6b2ab62e0d 100644 --- a/internal/command/user_v2_invite_model.go +++ b/internal/command/user_v2_invite_model.go @@ -28,7 +28,7 @@ type UserV2InviteWriteModel struct { } func (wm *UserV2InviteWriteModel) CreationAllowed() bool { - return !wm.EmailVerified && !wm.AuthMethodSet + return !wm.AuthMethodSet } func newUserV2InviteWriteModel(userID, orgID string) *UserV2InviteWriteModel { diff --git a/internal/command/user_v2_invite_test.go b/internal/command/user_v2_invite_test.go index efb57d86ad..75bd3157db 100644 --- a/internal/command/user_v2_invite_test.go +++ b/internal/command/user_v2_invite_test.go @@ -11,7 +11,6 @@ import ( "go.uber.org/mock/gomock" "golang.org/x/text/language" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -316,14 +315,28 @@ func TestCommands_ResendInviteCode(t *testing.T) { userID: "", }, want{ - err: zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing"), + err: zerrors.ThrowInvalidArgument(nil, "COMMAND-4jio3", "Errors.User.UserIDMissing"), }, }, { "missing permission", fields{ eventstore: expectEventstore( - expectFilter(), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + ), ), checkPermission: newMockPermissionCheckNotAllowed(), }, @@ -339,6 +352,7 @@ func TestCommands_ResendInviteCode(t *testing.T) { "user does not exist", fields{ eventstore: expectEventstore( + // The write model doesn't query any events expectFilter(), ), checkPermission: newMockPermissionCheckAllowed(), @@ -348,7 +362,7 @@ func TestCommands_ResendInviteCode(t *testing.T) { userID: "unknown", }, want{ - err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-H3b2a", "Errors.User.NotFound"), + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wgvn4", "Errors.User.NotFound"), }, }, { @@ -566,76 +580,6 @@ func TestCommands_ResendInviteCode(t *testing.T) { }, }, }, - { - "resend with own user ok", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("userID", "org1").Aggregate, - "username", "firstName", - "lastName", - "nickName", - "displayName", - language.Afrikaans, - domain.GenderUnspecified, - "email", - false, - ), - ), - eventFromEventPusher( - user.NewHumanInviteCodeAddedEvent(context.Background(), - &user.NewAggregate("userID", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("code"), - }, - time.Hour, - "", - false, - "", - "authRequestID", - ), - ), - ), - expectPush( - eventFromEventPusher( - user.NewHumanInviteCodeAddedEvent(authz.NewMockContext("instanceID", "org1", "userID"), - &user.NewAggregate("userID", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("code"), - }, - time.Hour, - "", - false, - "", - "authRequestID2", - ), - ), - ), - ), - checkPermission: newMockPermissionCheckNotAllowed(), // user does not have permission, is allowed in the own context - newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), - defaultSecretGenerators: &SecretGenerators{}, - }, - args{ - ctx: authz.NewMockContext("instanceID", "org1", "userID"), - userID: "userID", - authRequestID: "authRequestID2", - }, - want{ - details: &domain.ObjectDetails{ - ResourceOwner: "org1", - ID: "userID", - }, - }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/command/user_v2_machine.go b/internal/command/user_v2_machine.go new file mode 100644 index 0000000000..34079b7e6f --- /dev/null +++ b/internal/command/user_v2_machine.go @@ -0,0 +1,94 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type ChangeMachine struct { + ID string + ResourceOwner string + Username *string + Name *string + Description *string + + // Details are set after a successful execution of the command + Details *domain.ObjectDetails +} + +func (h *ChangeMachine) Changed() bool { + if h.Username != nil { + return true + } + if h.Name != nil { + return true + } + if h.Description != nil { + return true + } + return false +} + +func (c *Commands) ChangeUserMachine(ctx context.Context, machine *ChangeMachine) (err error) { + existingMachine, err := c.UserMachineWriteModel( + ctx, + machine.ID, + machine.ResourceOwner, + false, + ) + if err != nil { + return err + } + if machine.Changed() { + if err := c.checkPermissionUpdateUser(ctx, existingMachine.ResourceOwner, existingMachine.AggregateID); err != nil { + return err + } + } + + cmds := make([]eventstore.Command, 0) + if machine.Username != nil { + cmds, err = c.changeUsername(ctx, cmds, existingMachine, *machine.Username) + if err != nil { + return err + } + } + var machineChanges []user.MachineChanges + if machine.Name != nil && *machine.Name != existingMachine.Name { + machineChanges = append(machineChanges, user.ChangeName(*machine.Name)) + } + if machine.Description != nil && *machine.Description != existingMachine.Description { + machineChanges = append(machineChanges, user.ChangeDescription(*machine.Description)) + } + if len(machineChanges) > 0 { + cmds = append(cmds, user.NewMachineChangedEvent(ctx, &existingMachine.Aggregate().Aggregate, machineChanges)) + } + if len(cmds) == 0 { + machine.Details = writeModelToObjectDetails(&existingMachine.WriteModel) + return nil + } + err = c.pushAppendAndReduce(ctx, existingMachine, cmds...) + if err != nil { + return err + } + machine.Details = writeModelToObjectDetails(&existingMachine.WriteModel) + return nil +} + +func (c *Commands) UserMachineWriteModel(ctx context.Context, userID, resourceOwner string, metadataWM bool) (writeModel *UserV2WriteModel, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + writeModel = NewUserMachineWriteModel(userID, resourceOwner, metadataWM) + err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + if !isUserStateExists(writeModel.UserState) { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-ugjs0upun6", "Errors.User.NotFound") + } + return writeModel, nil +} diff --git a/internal/command/user_v2_machine_test.go b/internal/command/user_v2_machine_test.go new file mode 100644 index 0000000000..14df4bfae7 --- /dev/null +++ b/internal/command/user_v2_machine_test.go @@ -0,0 +1,260 @@ +package command + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommandSide_ChangeUserMachine(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + orgID string + machine *ChangeMachine + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + + userAgg := user.NewAggregate("user1", "org1") + userAddedEvent := user.NewMachineAddedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ) + + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "change machine username, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + name: "change machine username, not found", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-ugjs0upun6", "Errors.User.NotFound")) + }, + }, + }, + { + name: "change machine username, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &userAgg.Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewUsernameChangedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "changed", + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine username, no change", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("username"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine description, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Description: gu.Ptr("changed"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + name: "change machine description, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + expectPush( + user.NewMachineChangedEvent(context.Background(), + &userAgg.Aggregate, + []user.MachineChanges{ + user.ChangeDescription("changed"), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Description: gu.Ptr("changed"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine description, no change", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Description: gu.Ptr("description"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + err := r.ChangeUserMachine(tt.args.ctx, tt.args.machine) + if tt.res.err == nil { + if !assert.NoError(t, err) { + t.FailNow() + } + } else if !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + return + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, tt.args.machine.Details) + } + }) + } +} diff --git a/internal/command/user_v2_model.go b/internal/command/user_v2_model.go index 214a2a5f9d..92346bf3b6 100644 --- a/internal/command/user_v2_model.go +++ b/internal/command/user_v2_model.go @@ -118,6 +118,14 @@ func NewUserHumanWriteModel(userID, resourceOwner string, profileWM, emailWM, ph return newUserV2WriteModel(userID, resourceOwner, opts...) } +func NewUserMachineWriteModel(userID, resourceOwner string, metadataListWM bool) *UserV2WriteModel { + opts := []UserV2WMOption{WithMachine(), WithState()} + if metadataListWM { + opts = append(opts, WithMetadata()) + } + return newUserV2WriteModel(userID, resourceOwner, opts...) +} + func newUserV2WriteModel(userID, resourceOwner string, opts ...UserV2WMOption) *UserV2WriteModel { wm := &UserV2WriteModel{ WriteModel: eventstore.WriteModel{ diff --git a/internal/command/user_v2_test.go b/internal/command/user_v2_test.go index 3eb9ecd6f7..685ad95253 100644 --- a/internal/command/user_v2_test.go +++ b/internal/command/user_v2_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/org" @@ -1081,13 +1082,14 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) { } func TestCommandSide_RemoveUserV2(t *testing.T) { + ctxUserID := "ctxUserID" + ctx := authz.SetCtxData(context.Background(), authz.CtxData{UserID: ctxUserID}) type fields struct { eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck } type ( args struct { - ctx context.Context userID string cascadingMemberships []*CascadingMembership grantIDs []string @@ -1110,7 +1112,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), userID: "", }, res: res{ @@ -1128,7 +1129,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), userID: "user1", }, res: res{ @@ -1143,7 +1143,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - user.NewHumanAddedEvent(context.Background(), + user.NewHumanAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", @@ -1157,7 +1157,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), ), eventFromEventPusher( - user.NewUserRemovedEvent(context.Background(), + user.NewUserRemovedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", nil, @@ -1169,7 +1169,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), userID: "user1", }, res: res{ @@ -1184,7 +1183,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - user.NewHumanAddedEvent(context.Background(), + user.NewHumanAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", @@ -1200,7 +1199,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), expectFilter( eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), + org.NewDomainPolicyAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, true, true, @@ -1209,7 +1208,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), ), expectPush( - user.NewUserRemovedEvent(context.Background(), + user.NewUserRemovedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", nil, @@ -1220,7 +1219,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), userID: "user1", }, res: res{ @@ -1235,7 +1233,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - user.NewHumanAddedEvent(context.Background(), + user.NewHumanAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", @@ -1249,7 +1247,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), ), eventFromEventPusher( - user.NewHumanInitializedCheckSucceededEvent(context.Background(), + user.NewHumanInitializedCheckSucceededEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), @@ -1258,13 +1256,10 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - ctx: context.Background(), userID: "user1", }, res: res{ - err: func(err error) bool { - return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) - }, + err: zerrors.IsPermissionDenied, }, }, { @@ -1273,7 +1268,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - user.NewMachineAddedEvent(context.Background(), + user.NewMachineAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", "name", @@ -1283,7 +1278,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), ), eventFromEventPusher( - user.NewUserRemovedEvent(context.Background(), + user.NewUserRemovedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", nil, @@ -1292,10 +1287,8 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), userID: "user1", }, res: res{ @@ -1310,7 +1303,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - user.NewMachineAddedEvent(context.Background(), + user.NewMachineAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", "name", @@ -1322,7 +1315,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), expectFilter( eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), + org.NewDomainPolicyAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, true, true, @@ -1331,7 +1324,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), ), expectPush( - user.NewUserRemovedEvent(context.Background(), + user.NewUserRemovedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", nil, @@ -1342,7 +1335,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), userID: "user1", }, res: res{ @@ -1351,6 +1343,56 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { }, }, }, + { + name: "remove self, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(ctx, + &user.NewAggregate(ctxUserID, "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(ctx, + &user.NewAggregate(ctxUserID, "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewUserRemovedEvent(ctx, + &user.NewAggregate(ctxUserID, "org1").Aggregate, + "username", + nil, + true, + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: ctxUserID, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1358,7 +1400,8 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } - got, err := r.RemoveUserV2(tt.args.ctx, tt.args.userID, "", tt.args.cascadingMemberships, tt.args.grantIDs...) + + got, err := r.RemoveUserV2(ctx, tt.args.userID, "", tt.args.cascadingMemberships, tt.args.grantIDs...) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go index f6d39befe7..827dd61f73 100644 --- a/internal/config/systemdefaults/system_defaults.go +++ b/internal/config/systemdefaults/system_defaults.go @@ -7,15 +7,16 @@ import ( ) type SystemDefaults struct { - SecretGenerators SecretGenerators - PasswordHasher crypto.HashConfig - SecretHasher crypto.HashConfig - Multifactors MultifactorConfig - DomainVerification DomainVerification - Notifications Notifications - KeyConfig KeyConfig - DefaultQueryLimit uint64 - MaxQueryLimit uint64 + SecretGenerators SecretGenerators + PasswordHasher crypto.HashConfig + SecretHasher crypto.HashConfig + Multifactors MultifactorConfig + DomainVerification DomainVerification + Notifications Notifications + KeyConfig KeyConfig + DefaultQueryLimit uint64 + MaxQueryLimit uint64 + MaxIdPIntentLifetime time.Duration } type SecretGenerators struct { diff --git a/internal/crypto/passwap.go b/internal/crypto/passwap.go index e14c2dfaaf..e4f3313342 100644 --- a/internal/crypto/passwap.go +++ b/internal/crypto/passwap.go @@ -14,7 +14,9 @@ import ( "github.com/zitadel/passwap/md5plain" "github.com/zitadel/passwap/md5salted" "github.com/zitadel/passwap/pbkdf2" + "github.com/zitadel/passwap/phpass" "github.com/zitadel/passwap/scrypt" + "github.com/zitadel/passwap/sha2" "github.com/zitadel/passwap/verifier" "github.com/zitadel/zitadel/internal/zerrors" @@ -51,6 +53,8 @@ const ( 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 + HashNamePHPass HashName = "phpass" // verify only, as hashing with md5 is insecure and deprecated + HashNameSha2 HashName = "sha2" // hash and verify HashNameScrypt HashName = "scrypt" // hash and verify HashNamePBKDF2 HashName = "pbkdf2" // hash and verify ) @@ -125,6 +129,14 @@ var knowVerifiers = map[HashName]prefixVerifier{ prefixes: []string{md5salted.Prefix}, verifier: md5salted.Verifier, }, + HashNameSha2: { + prefixes: []string{sha2.Sha256Identifier, sha2.Sha512Identifier}, + verifier: sha2.Verifier, + }, + HashNamePHPass: { + prefixes: []string{phpass.IdentifierP, phpass.IdentifierH}, + verifier: phpass.Verifier, + }, } func (c *HashConfig) buildVerifiers() (verifiers []verifier.Verifier, prefixes []string, err error) { @@ -158,9 +170,11 @@ func (c *HasherConfig) buildHasher() (hasher passwap.Hasher, prefixes []string, return c.scrypt() case HashNamePBKDF2: return c.pbkdf2() + case HashNameSha2: + return c.sha2() case "": return nil, nil, fmt.Errorf("missing hasher algorithm") - case HashNameArgon2, HashNameMd5: + case HashNameArgon2, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted, HashNamePHPass: fallthrough default: return nil, nil, fmt.Errorf("invalid algorithm %q", c.Algorithm) @@ -296,6 +310,44 @@ func (c *HasherConfig) pbkdf2() (passwap.Hasher, []string, error) { case HashModeSHA512: return pbkdf2.NewSHA512(p), prefix, nil default: - return nil, nil, fmt.Errorf("unsuppored pbkdf2 hash mode: %s", hash) + return nil, nil, fmt.Errorf("unsupported pbkdf2 hash mode: %s", hash) + } +} + +func (c *HasherConfig) sha2Params() (use512 bool, rounds int, err error) { + var dst = struct { + Rounds uint32 `mapstructure:"Rounds"` + Hash HashMode `mapstructure:"Hash"` + }{} + if err := c.decodeParams(&dst); err != nil { + return false, 0, fmt.Errorf("decode sha2 params: %w", err) + } + switch dst.Hash { + case HashModeSHA256: + use512 = false + case HashModeSHA512: + use512 = true + case HashModeSHA1, HashModeSHA224, HashModeSHA384: + fallthrough + default: + return false, 0, fmt.Errorf("cannot use %s with sha2", dst.Hash) + } + if dst.Rounds > sha2.RoundsMax { + return false, 0, fmt.Errorf("rounds with sha2 cannot be larger than %d", sha2.RoundsMax) + } else { + rounds = int(dst.Rounds) + } + return use512, rounds, nil +} + +func (c *HasherConfig) sha2() (passwap.Hasher, []string, error) { + use512, rounds, err := c.sha2Params() + if err != nil { + return nil, nil, err + } + if use512 { + return sha2.New512(rounds), []string{sha2.Sha256Identifier, sha2.Sha512Identifier}, nil + } else { + return sha2.New256(rounds), []string{sha2.Sha256Identifier, sha2.Sha512Identifier}, nil } } diff --git a/internal/crypto/passwap_test.go b/internal/crypto/passwap_test.go index b872b0e298..dfcafd1406 100644 --- a/internal/crypto/passwap_test.go +++ b/internal/crypto/passwap_test.go @@ -14,6 +14,7 @@ import ( "github.com/zitadel/passwap/md5salted" "github.com/zitadel/passwap/pbkdf2" "github.com/zitadel/passwap/scrypt" + "github.com/zitadel/passwap/sha2" ) func TestPasswordHasher_EncodingSupported(t *testing.T) { @@ -78,7 +79,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { HashNameBcrypt, HashNameMd5, HashNameMd5Salted, + HashNamePHPass, HashNameScrypt, + HashNameSha2, "foobar", }, Hasher: HasherConfig{ @@ -142,6 +145,15 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { }, wantErr: true, }, + { + name: "invalid phpass", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNamePHPass, + }, + }, + wantErr: true, + }, { name: "invalid argon2", fields: fields{ @@ -357,6 +369,59 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { }, wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, + { + name: "sha2, parse error", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNameSha2, + Params: map[string]any{ + "cost": "bar", + }, + }, + }, + wantErr: true, + }, + { + name: "pbkdf2, hash mode error", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNameSha2, + Params: map[string]any{ + "Rounds": 12, + "Hash": "foo", + }, + }, + }, + wantErr: true, + }, + { + name: "sha2, sha256", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNameSha2, + Params: map[string]any{ + "Rounds": 12, + "Hash": HashModeSHA256, + }, + }, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain}, + }, + wantPrefixes: []string{sha2.Sha256Identifier, sha2.Sha512Identifier, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + }, + { + name: "sha2, sha512", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNameSha2, + Params: map[string]any{ + "Rounds": 12, + "Hash": HashModeSHA512, + }, + }, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain}, + }, + wantPrefixes: []string{sha2.Sha256Identifier, sha2.Sha512Identifier, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -723,3 +788,93 @@ func TestHasherConfig_pbkdf2Params(t *testing.T) { }) } } + +func TestHasherConfig_sha2Params(t *testing.T) { + type fields struct { + Params map[string]any + } + tests := []struct { + name string + fields fields + want512 bool + wantRounds int + wantErr bool + }{ + { + name: "decode error", + fields: fields{ + Params: map[string]any{ + "foo": "bar", + }, + }, + wantErr: true, + }, + { + name: "sha1", + fields: fields{ + Params: map[string]any{ + "Rounds": 12, + "Hash": "sha1", + }, + }, + wantErr: true, + }, + { + name: "sha224", + fields: fields{ + Params: map[string]any{ + "Rounds": 12, + "Hash": "sha224", + }, + }, + wantErr: true, + }, + { + name: "sha256", + fields: fields{ + Params: map[string]any{ + "Rounds": 5000, + "Hash": "sha256", + }, + }, + want512: false, + wantRounds: 5000, + }, + { + name: "sha384", + fields: fields{ + Params: map[string]any{ + "Rounds": 12, + "Hash": "sha384", + }, + }, + wantErr: true, + }, + { + name: "sha512", + fields: fields{ + Params: map[string]any{ + "Rounds": 15000, + "Hash": "sha512", + }, + }, + want512: true, + wantRounds: 15000, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &HasherConfig{ + Params: tt.fields.Params, + } + got512, gotRounds, err := c.sha2Params() + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want512, got512) + assert.Equal(t, tt.wantRounds, gotRounds) + }) + } +} diff --git a/internal/database/database.go b/internal/database/database.go index ddc26a7961..b40715d6b5 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -64,6 +64,10 @@ func CloseTransaction(tx Tx, err error) error { return commitErr } +const ( + PgUniqueConstraintErrorCode = "23505" +) + type Config struct { Dialects map[string]interface{} `mapstructure:",remain"` connector dialect.Connector diff --git a/internal/database/dialect/connections.go b/internal/database/dialect/connections.go index 11b2681fea..a5c90b4059 100644 --- a/internal/database/dialect/connections.go +++ b/internal/database/dialect/connections.go @@ -5,6 +5,7 @@ import ( "errors" "reflect" + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -23,7 +24,12 @@ type ConnectionConfig struct { AfterRelease []func(c *pgx.Conn) error } -var afterConnectFuncs []func(ctx context.Context, c *pgx.Conn) error +var afterConnectFuncs = []func(ctx context.Context, c *pgx.Conn) error{ + func(ctx context.Context, c *pgx.Conn) error { + pgxdecimal.Register(c.TypeMap()) + return nil + }, +} func RegisterAfterConnect(f func(ctx context.Context, c *pgx.Conn) error) { afterConnectFuncs = append(afterConnectFuncs, f) diff --git a/internal/domain/count_trigger.go b/internal/domain/count_trigger.go new file mode 100644 index 0000000000..a29d125fe9 --- /dev/null +++ b/internal/domain/count_trigger.go @@ -0,0 +1,9 @@ +package domain + +//go:generate enumer -type CountParentType -transform lower -trimprefix CountParentType -sql +type CountParentType int + +const ( + CountParentTypeInstance CountParentType = iota + CountParentTypeOrganization +) diff --git a/internal/domain/countparenttype_enumer.go b/internal/domain/countparenttype_enumer.go new file mode 100644 index 0000000000..8691d97e62 --- /dev/null +++ b/internal/domain/countparenttype_enumer.go @@ -0,0 +1,109 @@ +// Code generated by "enumer -type CountParentType -transform lower -trimprefix CountParentType -sql"; DO NOT EDIT. + +package domain + +import ( + "database/sql/driver" + "fmt" + "strings" +) + +const _CountParentTypeName = "instanceorganization" + +var _CountParentTypeIndex = [...]uint8{0, 8, 20} + +const _CountParentTypeLowerName = "instanceorganization" + +func (i CountParentType) String() string { + if i < 0 || i >= CountParentType(len(_CountParentTypeIndex)-1) { + return fmt.Sprintf("CountParentType(%d)", i) + } + return _CountParentTypeName[_CountParentTypeIndex[i]:_CountParentTypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _CountParentTypeNoOp() { + var x [1]struct{} + _ = x[CountParentTypeInstance-(0)] + _ = x[CountParentTypeOrganization-(1)] +} + +var _CountParentTypeValues = []CountParentType{CountParentTypeInstance, CountParentTypeOrganization} + +var _CountParentTypeNameToValueMap = map[string]CountParentType{ + _CountParentTypeName[0:8]: CountParentTypeInstance, + _CountParentTypeLowerName[0:8]: CountParentTypeInstance, + _CountParentTypeName[8:20]: CountParentTypeOrganization, + _CountParentTypeLowerName[8:20]: CountParentTypeOrganization, +} + +var _CountParentTypeNames = []string{ + _CountParentTypeName[0:8], + _CountParentTypeName[8:20], +} + +// CountParentTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func CountParentTypeString(s string) (CountParentType, error) { + if val, ok := _CountParentTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _CountParentTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to CountParentType values", s) +} + +// CountParentTypeValues returns all values of the enum +func CountParentTypeValues() []CountParentType { + return _CountParentTypeValues +} + +// CountParentTypeStrings returns a slice of all String values of the enum +func CountParentTypeStrings() []string { + strs := make([]string, len(_CountParentTypeNames)) + copy(strs, _CountParentTypeNames) + return strs +} + +// IsACountParentType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i CountParentType) IsACountParentType() bool { + for _, v := range _CountParentTypeValues { + if i == v { + return true + } + } + return false +} + +func (i CountParentType) Value() (driver.Value, error) { + return i.String(), nil +} + +func (i *CountParentType) 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 CountParentType: %[1]T(%[1]v)", value) + } + + val, err := CountParentTypeString(str) + if err != nil { + return err + } + + *i = val + return nil +} diff --git a/internal/domain/federatedlogout/logout.go b/internal/domain/federatedlogout/logout.go new file mode 100644 index 0000000000..ca208a129a --- /dev/null +++ b/internal/domain/federatedlogout/logout.go @@ -0,0 +1,37 @@ +package federatedlogout + +type Index int + +const ( + IndexUnspecified Index = iota + IndexRequestID +) + +type FederatedLogout struct { + InstanceID string + FingerPrintID string + SessionID string + IDPID string + UserID string + PostLogoutRedirectURI string + State State +} + +// Keys implements cache.Entry +func (c *FederatedLogout) Keys(i Index) []string { + if i == IndexRequestID { + return []string{Key(c.InstanceID, c.SessionID)} + } + return nil +} + +func Key(instanceID, sessionID string) string { + return instanceID + "-" + sessionID +} + +type State int + +const ( + StateCreated State = iota + StateRedirected +) diff --git a/internal/domain/idp.go b/internal/domain/idp.go index e2571f6b0d..bea106298b 100644 --- a/internal/domain/idp.go +++ b/internal/domain/idp.go @@ -115,6 +115,7 @@ const ( IDPIntentStateStarted IDPIntentStateSucceeded IDPIntentStateFailed + IDPIntentStateConsumed idpIntentStateCount ) diff --git a/internal/domain/permission.go b/internal/domain/permission.go index 0ddf08a664..bb569955f5 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -24,7 +24,7 @@ func (p *Permissions) appendPermission(ctxID, permission string) { p.Permissions = append(p.Permissions, permission) } -type PermissionCheck func(ctx context.Context, permission, orgID, resourceID string) (err error) +type PermissionCheck func(ctx context.Context, permission, resourceOwnerID, aggregateID string) (err error) const ( PermissionUserWrite = "user.write" @@ -38,6 +38,15 @@ const ( PermissionOrgRead = "org.read" PermissionIDPRead = "iam.idp.read" PermissionOrgIDPRead = "org.idp.read" + PermissionProjectWrite = "project.write" + PermissionProjectRead = "project.read" + PermissionProjectDelete = "project.delete" + PermissionProjectGrantWrite = "project.grant.write" + PermissionProjectGrantRead = "project.grant.read" + PermissionProjectGrantDelete = "project.grant.delete" + PermissionProjectRoleWrite = "project.role.write" + PermissionProjectRoleRead = "project.role.read" + PermissionProjectRoleDelete = "project.role.delete" ) // ProjectPermissionCheck is used as a check for preconditions dependent on application, project, user resourceowner and usergrants. diff --git a/internal/domain/project_grant.go b/internal/domain/project_grant.go index 31c6d9edbc..6376c9ed74 100644 --- a/internal/domain/project_grant.go +++ b/internal/domain/project_grant.go @@ -26,17 +26,12 @@ func (s ProjectGrantState) Valid() bool { return s > ProjectGrantStateUnspecified && s < projectGrantStateMax } -func (p *ProjectGrant) IsValid() bool { - return p.GrantedOrgID != "" +func (s ProjectGrantState) Exists() bool { + return s != ProjectGrantStateUnspecified && s != ProjectGrantStateRemoved } -func (g *ProjectGrant) HasInvalidRoles(validRoles []string) bool { - for _, roleKey := range g.RoleKeys { - if !containsRoleKey(roleKey, validRoles) { - return true - } - } - return false +func (p *ProjectGrant) IsValid() bool { + return p.GrantedOrgID != "" } func GetRemovedRoles(existingRoles, newRoles []string) []string { diff --git a/internal/domain/project_role.go b/internal/domain/project_role.go index e6c782bad1..c3593a1517 100644 --- a/internal/domain/project_role.go +++ b/internal/domain/project_role.go @@ -20,14 +20,23 @@ const ( ProjectRoleStateRemoved ) -func NewProjectRole(projectID, key string) *ProjectRole { - return &ProjectRole{ObjectRoot: models.ObjectRoot{AggregateID: projectID}, Key: key} +func (s ProjectRoleState) Exists() bool { + return s != ProjectRoleStateUnspecified && s != ProjectRoleStateRemoved } func (p *ProjectRole) IsValid() bool { return p.AggregateID != "" && p.Key != "" } +func HasInvalidRoles(validRoles, roles []string) bool { + for _, roleKey := range roles { + if !containsRoleKey(roleKey, validRoles) { + return true + } + } + return false +} + func containsRoleKey(roleKey string, validRoles []string) bool { for _, validRole := range validRoles { if roleKey == validRole { diff --git a/internal/domain/roles.go b/internal/domain/roles.go index b6bf2ffadd..c40eef6120 100644 --- a/internal/domain/roles.go +++ b/internal/domain/roles.go @@ -16,6 +16,7 @@ const ( RoleIAMOwner = "IAM_OWNER" RoleProjectOwner = "PROJECT_OWNER" RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL" + RoleProjectGrantOwner = "PROJECT_GRANT_OWNER" RoleSelfManagementGlobal = "SELF_MANAGEMENT_GLOBAL" ) diff --git a/internal/domain/secretgeneratortype_enumer.go b/internal/domain/secretgeneratortype_enumer.go index f819bafc1f..db66715670 100644 --- a/internal/domain/secretgeneratortype_enumer.go +++ b/internal/domain/secretgeneratortype_enumer.go @@ -4,11 +4,14 @@ package domain import ( "fmt" + "strings" ) -const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesecret_generator_type_count" +const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesigning_keysecret_generator_type_count" -var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 144, 171} +var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 144, 155, 182} + +const _SecretGeneratorTypeLowerName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesigning_keysecret_generator_type_count" func (i SecretGeneratorType) String() string { if i < 0 || i >= SecretGeneratorType(len(_SecretGeneratorTypeIndex)-1) { @@ -17,21 +20,70 @@ func (i SecretGeneratorType) String() string { return _SecretGeneratorTypeName[_SecretGeneratorTypeIndex[i]:_SecretGeneratorTypeIndex[i+1]] } -var _SecretGeneratorTypeValues = []SecretGeneratorType{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _SecretGeneratorTypeNoOp() { + var x [1]struct{} + _ = x[SecretGeneratorTypeUnspecified-(0)] + _ = x[SecretGeneratorTypeInitCode-(1)] + _ = x[SecretGeneratorTypeVerifyEmailCode-(2)] + _ = x[SecretGeneratorTypeVerifyPhoneCode-(3)] + _ = x[SecretGeneratorTypeVerifyDomain-(4)] + _ = x[SecretGeneratorTypePasswordResetCode-(5)] + _ = x[SecretGeneratorTypePasswordlessInitCode-(6)] + _ = x[SecretGeneratorTypeAppSecret-(7)] + _ = x[SecretGeneratorTypeOTPSMS-(8)] + _ = x[SecretGeneratorTypeOTPEmail-(9)] + _ = x[SecretGeneratorTypeInviteCode-(10)] + _ = x[SecretGeneratorTypeSigningKey-(11)] + _ = x[secretGeneratorTypeCount-(12)] +} + +var _SecretGeneratorTypeValues = []SecretGeneratorType{SecretGeneratorTypeUnspecified, SecretGeneratorTypeInitCode, SecretGeneratorTypeVerifyEmailCode, SecretGeneratorTypeVerifyPhoneCode, SecretGeneratorTypeVerifyDomain, SecretGeneratorTypePasswordResetCode, SecretGeneratorTypePasswordlessInitCode, SecretGeneratorTypeAppSecret, SecretGeneratorTypeOTPSMS, SecretGeneratorTypeOTPEmail, SecretGeneratorTypeInviteCode, SecretGeneratorTypeSigningKey, secretGeneratorTypeCount} var _SecretGeneratorTypeNameToValueMap = map[string]SecretGeneratorType{ - _SecretGeneratorTypeName[0:11]: 0, - _SecretGeneratorTypeName[11:20]: 1, - _SecretGeneratorTypeName[20:37]: 2, - _SecretGeneratorTypeName[37:54]: 3, - _SecretGeneratorTypeName[54:67]: 4, - _SecretGeneratorTypeName[67:86]: 5, - _SecretGeneratorTypeName[86:108]: 6, - _SecretGeneratorTypeName[108:118]: 7, - _SecretGeneratorTypeName[118:124]: 8, - _SecretGeneratorTypeName[124:133]: 9, - _SecretGeneratorTypeName[133:144]: 10, - _SecretGeneratorTypeName[144:171]: 11, + _SecretGeneratorTypeName[0:11]: SecretGeneratorTypeUnspecified, + _SecretGeneratorTypeLowerName[0:11]: SecretGeneratorTypeUnspecified, + _SecretGeneratorTypeName[11:20]: SecretGeneratorTypeInitCode, + _SecretGeneratorTypeLowerName[11:20]: SecretGeneratorTypeInitCode, + _SecretGeneratorTypeName[20:37]: SecretGeneratorTypeVerifyEmailCode, + _SecretGeneratorTypeLowerName[20:37]: SecretGeneratorTypeVerifyEmailCode, + _SecretGeneratorTypeName[37:54]: SecretGeneratorTypeVerifyPhoneCode, + _SecretGeneratorTypeLowerName[37:54]: SecretGeneratorTypeVerifyPhoneCode, + _SecretGeneratorTypeName[54:67]: SecretGeneratorTypeVerifyDomain, + _SecretGeneratorTypeLowerName[54:67]: SecretGeneratorTypeVerifyDomain, + _SecretGeneratorTypeName[67:86]: SecretGeneratorTypePasswordResetCode, + _SecretGeneratorTypeLowerName[67:86]: SecretGeneratorTypePasswordResetCode, + _SecretGeneratorTypeName[86:108]: SecretGeneratorTypePasswordlessInitCode, + _SecretGeneratorTypeLowerName[86:108]: SecretGeneratorTypePasswordlessInitCode, + _SecretGeneratorTypeName[108:118]: SecretGeneratorTypeAppSecret, + _SecretGeneratorTypeLowerName[108:118]: SecretGeneratorTypeAppSecret, + _SecretGeneratorTypeName[118:124]: SecretGeneratorTypeOTPSMS, + _SecretGeneratorTypeLowerName[118:124]: SecretGeneratorTypeOTPSMS, + _SecretGeneratorTypeName[124:133]: SecretGeneratorTypeOTPEmail, + _SecretGeneratorTypeLowerName[124:133]: SecretGeneratorTypeOTPEmail, + _SecretGeneratorTypeName[133:144]: SecretGeneratorTypeInviteCode, + _SecretGeneratorTypeLowerName[133:144]: SecretGeneratorTypeInviteCode, + _SecretGeneratorTypeName[144:155]: SecretGeneratorTypeSigningKey, + _SecretGeneratorTypeLowerName[144:155]: SecretGeneratorTypeSigningKey, + _SecretGeneratorTypeName[155:182]: secretGeneratorTypeCount, + _SecretGeneratorTypeLowerName[155:182]: secretGeneratorTypeCount, +} + +var _SecretGeneratorTypeNames = []string{ + _SecretGeneratorTypeName[0:11], + _SecretGeneratorTypeName[11:20], + _SecretGeneratorTypeName[20:37], + _SecretGeneratorTypeName[37:54], + _SecretGeneratorTypeName[54:67], + _SecretGeneratorTypeName[67:86], + _SecretGeneratorTypeName[86:108], + _SecretGeneratorTypeName[108:118], + _SecretGeneratorTypeName[118:124], + _SecretGeneratorTypeName[124:133], + _SecretGeneratorTypeName[133:144], + _SecretGeneratorTypeName[144:155], + _SecretGeneratorTypeName[155:182], } // SecretGeneratorTypeString retrieves an enum value from the enum constants string name. @@ -40,6 +92,10 @@ func SecretGeneratorTypeString(s string) (SecretGeneratorType, error) { if val, ok := _SecretGeneratorTypeNameToValueMap[s]; ok { return val, nil } + + if val, ok := _SecretGeneratorTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } return 0, fmt.Errorf("%s does not belong to SecretGeneratorType values", s) } @@ -48,6 +104,13 @@ func SecretGeneratorTypeValues() []SecretGeneratorType { return _SecretGeneratorTypeValues } +// SecretGeneratorTypeStrings returns a slice of all String values of the enum +func SecretGeneratorTypeStrings() []string { + strs := make([]string, len(_SecretGeneratorTypeNames)) + copy(strs, _SecretGeneratorTypeNames) + return strs +} + // IsASecretGeneratorType returns "true" if the value is listed in the enum definition. "false" otherwise func (i SecretGeneratorType) IsASecretGeneratorType() bool { for _, v := range _SecretGeneratorTypeValues { diff --git a/internal/eventstore/event.go b/internal/eventstore/event.go index 3df096f069..656a02f33d 100644 --- a/internal/eventstore/event.go +++ b/internal/eventstore/event.go @@ -5,6 +5,8 @@ import ( "reflect" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/zerrors" ) @@ -44,7 +46,7 @@ type Event interface { // CreatedAt is the time the event was created at CreatedAt() time.Time // Position is the global position of the event - Position() float64 + Position() decimal.Decimal // Unmarshal parses the payload and stores the result // in the value pointed to by ptr. If ptr is nil or not a pointer, diff --git a/internal/eventstore/event_base.go b/internal/eventstore/event_base.go index ed81e95320..6a911bc0eb 100644 --- a/internal/eventstore/event_base.go +++ b/internal/eventstore/event_base.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -25,7 +26,7 @@ type BaseEvent struct { Agg *Aggregate `json:"-"` Seq uint64 - Pos float64 + Pos decimal.Decimal Creation time.Time previousAggregateSequence uint64 previousAggregateTypeSequence uint64 @@ -38,7 +39,7 @@ type BaseEvent struct { } // Position implements Event. -func (e *BaseEvent) Position() float64 { +func (e *BaseEvent) Position() decimal.Decimal { return e.Pos } diff --git a/internal/eventstore/eventstore.go b/internal/eventstore/eventstore.go index 4954df86c8..8a8d32bc43 100644 --- a/internal/eventstore/eventstore.go +++ b/internal/eventstore/eventstore.go @@ -7,6 +7,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -14,6 +15,12 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +func init() { + // this is needed to ensure that position is marshaled as a number + // otherwise it will be marshaled as a string + decimal.MarshalJSONWithoutQuotes = true +} + // Eventstore abstracts all functions needed to store valid events // and filters the stored events type Eventstore struct { @@ -229,11 +236,11 @@ func (es *Eventstore) FilterToReducer(ctx context.Context, searchQuery *SearchQu }) } -// LatestSequence filters the latest sequence for the given search query -func (es *Eventstore) LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) { +// LatestPosition filters the latest position for the given search query +func (es *Eventstore) LatestPosition(ctx context.Context, queryFactory *SearchQueryBuilder) (decimal.Decimal, error) { queryFactory.InstanceID(authz.GetInstance(ctx).InstanceID()) - return es.querier.LatestSequence(ctx, queryFactory) + return es.querier.LatestPosition(ctx, queryFactory) } // InstanceIDs returns the distinct instance ids found by the search query @@ -265,8 +272,8 @@ type Querier interface { Health(ctx context.Context) error // FilterToReducer calls r for every event returned from the storage FilterToReducer(ctx context.Context, searchQuery *SearchQueryBuilder, r Reducer) error - // LatestSequence returns the latest sequence found by the search query - LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) + // LatestPosition returns the latest position found by the search query + LatestPosition(ctx context.Context, queryFactory *SearchQueryBuilder) (decimal.Decimal, error) // InstanceIDs returns the instance ids found by the search query InstanceIDs(ctx context.Context, queryFactory *SearchQueryBuilder) ([]string, error) // Client returns the underlying database connection diff --git a/internal/eventstore/eventstore_querier_test.go b/internal/eventstore/eventstore_querier_test.go index 3f23c5da75..88797a835e 100644 --- a/internal/eventstore/eventstore_querier_test.go +++ b/internal/eventstore/eventstore_querier_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" ) @@ -131,7 +133,7 @@ func TestEventstore_Filter(t *testing.T) { } } -func TestEventstore_LatestSequence(t *testing.T) { +func TestEventstore_LatestPosition(t *testing.T) { type args struct { searchQuery *eventstore.SearchQueryBuilder } @@ -139,7 +141,7 @@ func TestEventstore_LatestSequence(t *testing.T) { existingEvents []eventstore.Command } type res struct { - sequence float64 + position decimal.Decimal } tests := []struct { name string @@ -151,7 +153,7 @@ func TestEventstore_LatestSequence(t *testing.T) { { name: "aggregate type filter no sequence", args: args{ - searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). + searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). AddQuery(). AggregateTypes("not found"). Builder(), @@ -168,7 +170,7 @@ func TestEventstore_LatestSequence(t *testing.T) { { name: "aggregate type filter sequence", args: args{ - searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). + searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). AddQuery(). AggregateTypes(eventstore.AggregateType(t.Name())). Builder(), @@ -202,12 +204,12 @@ func TestEventstore_LatestSequence(t *testing.T) { return } - sequence, err := db.LatestSequence(context.Background(), tt.args.searchQuery) + position, err := db.LatestPosition(context.Background(), tt.args.searchQuery) if (err != nil) != tt.wantErr { t.Errorf("eventstore.query() error = %v, wantErr %v", err, tt.wantErr) } - if tt.res.sequence > sequence { - t.Errorf("eventstore.query() expected sequence: %v got %v", tt.res.sequence, sequence) + if tt.res.position.GreaterThan(position) { + t.Errorf("eventstore.query() expected position: %v got %v", tt.res.position, position) } }) } diff --git a/internal/eventstore/eventstore_test.go b/internal/eventstore/eventstore_test.go index 9e1aa77db1..5452572faa 100644 --- a/internal/eventstore/eventstore_test.go +++ b/internal/eventstore/eventstore_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/service" @@ -397,7 +398,7 @@ func (repo *testPusher) Push(_ context.Context, _ database.ContextQueryExecuter, type testQuerier struct { events []Event - sequence float64 + sequence decimal.Decimal instances []string err error t *testing.T @@ -430,9 +431,9 @@ func (repo *testQuerier) FilterToReducer(ctx context.Context, searchQuery *Searc return nil } -func (repo *testQuerier) LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) { +func (repo *testQuerier) LatestPosition(ctx context.Context, queryFactory *SearchQueryBuilder) (decimal.Decimal, error) { if repo.err != nil { - return 0, repo.err + return decimal.Decimal{}, repo.err } return repo.sequence, nil } @@ -1076,7 +1077,7 @@ func TestEventstore_FilterEvents(t *testing.T) { } } -func TestEventstore_LatestSequence(t *testing.T) { +func TestEventstore_LatestPosition(t *testing.T) { type args struct { query *SearchQueryBuilder } @@ -1096,7 +1097,7 @@ func TestEventstore_LatestSequence(t *testing.T) { name: "no events", args: args{ query: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, queries: []*SearchQuery{ { builder: &SearchQueryBuilder{}, @@ -1119,7 +1120,7 @@ func TestEventstore_LatestSequence(t *testing.T) { name: "repo error", args: args{ query: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, queries: []*SearchQuery{ { builder: &SearchQueryBuilder{}, @@ -1142,7 +1143,7 @@ func TestEventstore_LatestSequence(t *testing.T) { name: "found events", args: args{ query: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, queries: []*SearchQuery{ { builder: &SearchQueryBuilder{}, @@ -1168,7 +1169,7 @@ func TestEventstore_LatestSequence(t *testing.T) { querier: tt.fields.repo, } - _, err := es.LatestSequence(context.Background(), tt.args.query) + _, err := es.LatestPosition(context.Background(), tt.args.query) if (err != nil) != tt.res.wantErr { t.Errorf("Eventstore.aggregatesToEvents() error = %v, wantErr %v", err, tt.res.wantErr) } diff --git a/internal/eventstore/handler/v2/field_handler.go b/internal/eventstore/handler/v2/field_handler.go index ad309ac790..3c25731c83 100644 --- a/internal/eventstore/handler/v2/field_handler.go +++ b/internal/eventstore/handler/v2/field_handler.go @@ -8,6 +8,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/eventstore" ) @@ -126,10 +127,15 @@ func (h *FieldHandler) processEvents(ctx context.Context, config *triggerConfig) return additionalIteration, err } // stop execution if currentState.eventTimestamp >= config.maxCreatedAt - if config.maxPosition != 0 && currentState.position >= config.maxPosition { + if !config.maxPosition.IsZero() && currentState.position.GreaterThanOrEqual(config.maxPosition) { return false, nil } + if config.minPosition.GreaterThan(decimal.NewFromInt(0)) { + currentState.position = config.minPosition + currentState.offset = 0 + } + events, additionalIteration, err := h.fetchEvents(ctx, tx, currentState) if err != nil { return additionalIteration, err @@ -159,7 +165,7 @@ func (h *FieldHandler) fetchEvents(ctx context.Context, tx *sql.Tx, currentState idx, offset := skipPreviouslyReducedEvents(events, currentState) - if currentState.position == events[len(events)-1].Position() { + if currentState.position.Equal(events[len(events)-1].Position()) { offset += currentState.offset } currentState.position = events[len(events)-1].Position() @@ -179,7 +185,7 @@ func (h *FieldHandler) fetchEvents(ctx context.Context, tx *sql.Tx, currentState fillFieldsEvents := make([]eventstore.FillFieldsEvent, len(events)) highestPosition := events[len(events)-1].Position() for i, event := range events { - if event.Position() == highestPosition { + if event.Position().Equal(highestPosition) { offset++ } fillFieldsEvents[i] = event.(eventstore.FillFieldsEvent) @@ -189,14 +195,14 @@ func (h *FieldHandler) fetchEvents(ctx context.Context, tx *sql.Tx, currentState } func skipPreviouslyReducedEvents(events []eventstore.Event, currentState *state) (index int, offset uint32) { - var position float64 + var position decimal.Decimal for i, event := range events { - if event.Position() != position { + if !event.Position().Equal(position) { offset = 0 position = event.Position() } offset++ - if event.Position() == currentState.position && + if event.Position().Equal(currentState.position) && event.Aggregate().ID == currentState.aggregateID && event.Aggregate().Type == currentState.aggregateType && event.Sequence() == currentState.sequence { diff --git a/internal/eventstore/handler/v2/handler.go b/internal/eventstore/handler/v2/handler.go index 43c3e58b3b..fd8b206b38 100644 --- a/internal/eventstore/handler/v2/handler.go +++ b/internal/eventstore/handler/v2/handler.go @@ -4,13 +4,13 @@ import ( "context" "database/sql" "errors" - "math" "math/rand" "slices" "sync" "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -60,6 +60,7 @@ type Handler struct { requeueEvery time.Duration txDuration time.Duration now nowFunc + queryGlobal bool triggeredInstancesSync sync.Map @@ -143,6 +144,11 @@ type Projection interface { Reducers() []AggregateReducer } +type GlobalProjection interface { + Projection + FilterGlobalEvents() +} + func NewHandler( ctx context.Context, config *Config, @@ -185,6 +191,10 @@ func NewHandler( metrics: metrics, } + if _, ok := projection.(GlobalProjection); ok { + handler.queryGlobal = true + } + return handler } @@ -385,7 +395,8 @@ func (h *Handler) existingInstances(ctx context.Context) ([]string, error) { type triggerConfig struct { awaitRunning bool - maxPosition float64 + maxPosition decimal.Decimal + minPosition decimal.Decimal } type TriggerOpt func(conf *triggerConfig) @@ -396,12 +407,18 @@ func WithAwaitRunning() TriggerOpt { } } -func WithMaxPosition(position float64) TriggerOpt { +func WithMaxPosition(position decimal.Decimal) TriggerOpt { return func(conf *triggerConfig) { conf.maxPosition = position } } +func WithMinPosition(position decimal.Decimal) TriggerOpt { + return func(conf *triggerConfig) { + conf.minPosition = position + } +} + func (h *Handler) Trigger(ctx context.Context, opts ...TriggerOpt) (_ context.Context, err error) { config := new(triggerConfig) for _, opt := range opts { @@ -510,10 +527,15 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add return additionalIteration, err } // stop execution if currentState.position >= config.maxPosition - if config.maxPosition != 0 && currentState.position >= config.maxPosition { + if !config.maxPosition.Equal(decimal.Decimal{}) && currentState.position.GreaterThanOrEqual(config.maxPosition) { return false, nil } + if config.minPosition.GreaterThan(decimal.NewFromInt(0)) { + currentState.position = config.minPosition + currentState.offset = 0 + } + var statements []*Statement statements, additionalIteration, err = h.generateStatements(ctx, tx, currentState) if err != nil { @@ -555,7 +577,10 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add currentState.sequence = statements[lastProcessedIndex].Sequence currentState.eventTimestamp = statements[lastProcessedIndex].CreationDate - err = h.setState(tx, currentState) + setStateErr := h.setState(tx, currentState) + if setStateErr != nil { + err = setStateErr + } return additionalIteration, err } @@ -605,7 +630,7 @@ func (h *Handler) generateStatements(ctx context.Context, tx *sql.Tx, currentSta func skipPreviouslyReducedStatements(statements []*Statement, currentState *state) int { for i, statement := range statements { - if statement.Position == currentState.position && + if statement.Position.Equal(currentState.position) && statement.Aggregate.ID == currentState.aggregateID && statement.Aggregate.Type == currentState.aggregateType && statement.Sequence == currentState.sequence { @@ -668,14 +693,17 @@ func (h *Handler) eventQuery(currentState *state) *eventstore.SearchQueryBuilder OrderAsc(). InstanceID(currentState.instanceID) - if currentState.position > 0 { - // decrease position by 10 because builder.PositionAfter filters for position > and we need position >= - builder = builder.PositionAfter(math.Float64frombits(math.Float64bits(currentState.position) - 10)) + if currentState.position.GreaterThan(decimal.Decimal{}) { + builder = builder.PositionAtLeast(currentState.position) if currentState.offset > 0 { builder = builder.Offset(currentState.offset) } } + if h.queryGlobal { + return builder + } + aggregateTypes := make([]eventstore.AggregateType, 0, len(h.eventTypes)) eventTypes := make([]eventstore.EventType, 0, len(h.eventTypes)) diff --git a/internal/eventstore/handler/v2/state.go b/internal/eventstore/handler/v2/state.go index b81719ca55..d5ed3a2d10 100644 --- a/internal/eventstore/handler/v2/state.go +++ b/internal/eventstore/handler/v2/state.go @@ -7,6 +7,8 @@ import ( "errors" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" @@ -14,7 +16,7 @@ import ( type state struct { instanceID string - position float64 + position decimal.Decimal eventTimestamp time.Time aggregateType eventstore.AggregateType aggregateID string @@ -45,7 +47,7 @@ func (h *Handler) currentState(ctx context.Context, tx *sql.Tx, config *triggerC aggregateType = new(sql.NullString) sequence = new(sql.NullInt64) timestamp = new(sql.NullTime) - position = new(sql.NullFloat64) + position = new(decimal.NullDecimal) offset = new(sql.NullInt64) ) @@ -75,7 +77,7 @@ func (h *Handler) currentState(ctx context.Context, tx *sql.Tx, config *triggerC currentState.aggregateType = eventstore.AggregateType(aggregateType.String) currentState.sequence = uint64(sequence.Int64) currentState.eventTimestamp = timestamp.Time - currentState.position = position.Float64 + currentState.position = position.Decimal // psql does not provide unsigned numbers so we work around it currentState.offset = uint32(offset.Int64) return currentState, nil diff --git a/internal/eventstore/handler/v2/state_test.go b/internal/eventstore/handler/v2/state_test.go index cc5fb1fbab..ef91d78e55 100644 --- a/internal/eventstore/handler/v2/state_test.go +++ b/internal/eventstore/handler/v2/state_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database/mock" @@ -166,7 +167,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { updatedState: &state{ instanceID: "instance", eventTimestamp: time.Now(), - position: 42, + position: decimal.NewFromInt(42), }, }, isErr: func(t *testing.T, err error) { @@ -192,7 +193,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { updatedState: &state{ instanceID: "instance", eventTimestamp: time.Now(), - position: 42, + position: decimal.NewFromInt(42), }, }, isErr: func(t *testing.T, err error) { @@ -217,7 +218,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { eventstore.AggregateType("aggregate type"), uint64(42), mock.AnyType[time.Time]{}, - float64(42), + decimal.NewFromInt(42), uint32(0), ), mock.WithExecRowsAffected(1), @@ -228,7 +229,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { updatedState: &state{ instanceID: "instance", eventTimestamp: time.Now(), - position: 42, + position: decimal.NewFromInt(42), aggregateType: "aggregate type", aggregateID: "aggregate id", sequence: 42, @@ -397,7 +398,7 @@ func TestHandler_currentState(t *testing.T) { "aggregate type", int64(42), testTime, - float64(42), + decimal.NewFromInt(42).String(), uint16(10), }, }, @@ -412,7 +413,7 @@ func TestHandler_currentState(t *testing.T) { currentState: &state{ instanceID: "instance", eventTimestamp: testTime, - position: 42, + position: decimal.NewFromInt(42), aggregateType: "aggregate type", aggregateID: "aggregate id", sequence: 42, diff --git a/internal/eventstore/handler/v2/statement.go b/internal/eventstore/handler/v2/statement.go index a02e5d3580..5024c8c945 100644 --- a/internal/eventstore/handler/v2/statement.go +++ b/internal/eventstore/handler/v2/statement.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "golang.org/x/exp/constraints" @@ -52,7 +53,7 @@ func (h *Handler) eventsToStatements(tx *sql.Tx, events []eventstore.Event, curr return statements, err } offset++ - if previousPosition != event.Position() { + if !previousPosition.Equal(event.Position()) { // offset is 1 because we want to skip this event offset = 1 } @@ -82,7 +83,7 @@ func (h *Handler) reduce(event eventstore.Event) (*Statement, error) { type Statement struct { Aggregate *eventstore.Aggregate Sequence uint64 - Position float64 + Position decimal.Decimal CreationDate time.Time offset uint32 diff --git a/internal/eventstore/local_postgres_test.go b/internal/eventstore/local_postgres_test.go index d75292b3ff..fdb8b4f516 100644 --- a/internal/eventstore/local_postgres_test.go +++ b/internal/eventstore/local_postgres_test.go @@ -2,12 +2,13 @@ package eventstore_test import ( "context" - "database/sql" "encoding/json" "os" "testing" "time" + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/zitadel/logging" @@ -40,7 +41,10 @@ func TestMain(m *testing.M) { connConfig, err := pgxpool.ParseConfig(config.GetConnectionURL()) logging.OnError(err).Fatal("unable to parse db url") - connConfig.AfterConnect = new_es.RegisterEventstoreTypes + connConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return new_es.RegisterEventstoreTypes(ctx, conn) + } pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) logging.OnError(err).Fatal("unable to create db pool") @@ -101,10 +105,19 @@ func initDB(ctx context.Context, db *database.DB) error { } func connectLocalhost() (*database.DB, error) { - client, err := sql.Open("pgx", "postgresql://postgres@localhost:5432/postgres?sslmode=disable") + config, err := pgxpool.ParseConfig("postgresql://postgres@localhost:5432/postgres?sslmode=disable") if err != nil { return nil, err } + config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return new_es.RegisterEventstoreTypes(ctx, conn) + } + pool, err := pgxpool.NewWithConfig(context.Background(), config) + if err != nil { + return nil, err + } + client := stdlib.OpenDBFromPool(pool) if err = client.Ping(); err != nil { return nil, err } diff --git a/internal/eventstore/read_model.go b/internal/eventstore/read_model.go index d2c755cc3a..ae77275732 100644 --- a/internal/eventstore/read_model.go +++ b/internal/eventstore/read_model.go @@ -1,19 +1,23 @@ package eventstore -import "time" +import ( + "time" + + "github.com/shopspring/decimal" +) // ReadModel is the minimum representation of a read model. // It implements a basic reducer // it might be saved in a database or in memory type ReadModel struct { - AggregateID string `json:"-"` - ProcessedSequence uint64 `json:"-"` - CreationDate time.Time `json:"-"` - ChangeDate time.Time `json:"-"` - Events []Event `json:"-"` - ResourceOwner string `json:"-"` - InstanceID string `json:"-"` - Position float64 `json:"-"` + AggregateID string `json:"-"` + ProcessedSequence uint64 `json:"-"` + CreationDate time.Time `json:"-"` + ChangeDate time.Time `json:"-"` + Events []Event `json:"-"` + ResourceOwner string `json:"-"` + InstanceID string `json:"-"` + Position decimal.Decimal `json:"-"` } // AppendEvents adds all the events to the read model. diff --git a/internal/eventstore/repository/event.go b/internal/eventstore/repository/event.go index d0d2660d79..1107649934 100644 --- a/internal/eventstore/repository/event.go +++ b/internal/eventstore/repository/event.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/eventstore" @@ -22,7 +23,7 @@ type Event struct { // Seq is the sequence of the event Seq uint64 // Pos is the global sequence of the event multiple events can have the same sequence - Pos float64 + Pos decimal.Decimal //CreationDate is the time the event is created // it's used for human readability. @@ -97,7 +98,7 @@ func (e *Event) Sequence() uint64 { } // Position implements [eventstore.Event] -func (e *Event) Position() float64 { +func (e *Event) Position() decimal.Decimal { return e.Pos } diff --git a/internal/eventstore/repository/mock/repository.mock.go b/internal/eventstore/repository/mock/repository.mock.go index 8d5c0430ad..12925bc975 100644 --- a/internal/eventstore/repository/mock/repository.mock.go +++ b/internal/eventstore/repository/mock/repository.mock.go @@ -13,6 +13,7 @@ import ( context "context" reflect "reflect" + decimal "github.com/shopspring/decimal" database "github.com/zitadel/zitadel/internal/database" eventstore "github.com/zitadel/zitadel/internal/eventstore" gomock "go.uber.org/mock/gomock" @@ -98,19 +99,19 @@ func (mr *MockQuerierMockRecorder) InstanceIDs(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceIDs", reflect.TypeOf((*MockQuerier)(nil).InstanceIDs), arg0, arg1) } -// LatestSequence mocks base method. -func (m *MockQuerier) LatestSequence(arg0 context.Context, arg1 *eventstore.SearchQueryBuilder) (float64, error) { +// LatestPosition mocks base method. +func (m *MockQuerier) LatestPosition(arg0 context.Context, arg1 *eventstore.SearchQueryBuilder) (decimal.Decimal, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LatestSequence", arg0, arg1) - ret0, _ := ret[0].(float64) + ret := m.ctrl.Call(m, "LatestPosition", arg0, arg1) + ret0, _ := ret[0].(decimal.Decimal) ret1, _ := ret[1].(error) return ret0, ret1 } -// LatestSequence indicates an expected call of LatestSequence. -func (mr *MockQuerierMockRecorder) LatestSequence(arg0, arg1 any) *gomock.Call { +// LatestPosition indicates an expected call of LatestPosition. +func (mr *MockQuerierMockRecorder) LatestPosition(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatestSequence", reflect.TypeOf((*MockQuerier)(nil).LatestSequence), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatestPosition", reflect.TypeOf((*MockQuerier)(nil).LatestPosition), arg0, arg1) } // MockPusher is a mock of Pusher interface. diff --git a/internal/eventstore/repository/mock/repository.mock.impl.go b/internal/eventstore/repository/mock/repository.mock.impl.go index ced76953cb..313f7ee5e8 100644 --- a/internal/eventstore/repository/mock/repository.mock.impl.go +++ b/internal/eventstore/repository/mock/repository.mock.impl.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -197,8 +198,8 @@ func (e *mockEvent) Sequence() uint64 { return e.sequence } -func (e *mockEvent) Position() float64 { - return 0 +func (e *mockEvent) Position() decimal.Decimal { + return decimal.Decimal{} } func (e *mockEvent) CreatedAt() time.Time { diff --git a/internal/eventstore/repository/search_query.go b/internal/eventstore/repository/search_query.go index 6ffba31ca8..760f7f616c 100644 --- a/internal/eventstore/repository/search_query.go +++ b/internal/eventstore/repository/search_query.go @@ -3,6 +3,8 @@ package repository import ( "database/sql" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" @@ -57,6 +59,8 @@ const ( // OperationNotIn checks if a stored value does not match one of the passed value list OperationNotIn + OperationGreaterOrEquals + operationCount ) @@ -250,10 +254,10 @@ func instanceIDsFilter(builder *eventstore.SearchQueryBuilder, query *SearchQuer } func positionAfterFilter(builder *eventstore.SearchQueryBuilder, query *SearchQuery) *Filter { - if builder.GetPositionAfter() == 0 { + if builder.GetPositionAtLeast().IsZero() { return nil } - query.Position = NewFilter(FieldPosition, builder.GetPositionAfter(), OperationGreater) + query.Position = NewFilter(FieldPosition, builder.GetPositionAtLeast(), OperationGreaterOrEquals) return query.Position } @@ -295,7 +299,7 @@ func eventDataFilter(query *eventstore.SearchQuery) *Filter { } func eventPositionAfterFilter(query *eventstore.SearchQuery) *Filter { - if pos := query.GetPositionAfter(); pos != 0 { + if pos := query.GetPositionAfter(); !pos.Equal(decimal.Decimal{}) { return NewFilter(FieldPosition, pos, OperationGreater) } return nil diff --git a/internal/eventstore/repository/sql/local_postgres_test.go b/internal/eventstore/repository/sql/local_postgres_test.go index 765da213e3..ae1f7b4831 100644 --- a/internal/eventstore/repository/sql/local_postgres_test.go +++ b/internal/eventstore/repository/sql/local_postgres_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/zitadel/logging" @@ -30,7 +32,11 @@ func TestMain(m *testing.M) { connConfig, err := pgxpool.ParseConfig(config.GetConnectionURL()) logging.OnError(err).Fatal("unable to parse db url") - connConfig.AfterConnect = new_es.RegisterEventstoreTypes + connConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return new_es.RegisterEventstoreTypes(ctx, conn) + } + pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) logging.OnError(err).Fatal("unable to create db pool") diff --git a/internal/eventstore/repository/sql/postgres.go b/internal/eventstore/repository/sql/postgres.go index bc9ad2e029..0dc2210f7b 100644 --- a/internal/eventstore/repository/sql/postgres.go +++ b/internal/eventstore/repository/sql/postgres.go @@ -2,12 +2,12 @@ package sql import ( "context" - "database/sql" "errors" "regexp" "strconv" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" @@ -55,11 +55,11 @@ func (psql *Postgres) FilterToReducer(ctx context.Context, searchQuery *eventsto 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 +// LatestPosition returns the latest position found by the search query +func (db *Postgres) LatestPosition(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) (decimal.Decimal, error) { + var position decimal.Decimal err := query(ctx, db, searchQuery, &position, false) - return position.Float64, err + return position, err } // InstanceIDs returns the instance ids found by the search query @@ -126,7 +126,7 @@ func (db *Postgres) eventQuery(useV1 bool) string { " FROM eventstore.events2" } -func (db *Postgres) maxSequenceQuery(useV1 bool) string { +func (db *Postgres) maxPositionQuery(useV1 bool) string { if useV1 { return `SELECT event_sequence FROM eventstore.events` } @@ -207,6 +207,8 @@ func (db *Postgres) operation(operation repository.Operation) string { return "=" case repository.OperationGreater: return ">" + case repository.OperationGreaterOrEquals: + return ">=" case repository.OperationLess: return "<" case repository.OperationJSONContains: diff --git a/internal/eventstore/repository/sql/postgres_test.go b/internal/eventstore/repository/sql/postgres_test.go index 151fdd1b6a..8a9b7bc049 100644 --- a/internal/eventstore/repository/sql/postgres_test.go +++ b/internal/eventstore/repository/sql/postgres_test.go @@ -4,6 +4,8 @@ import ( "database/sql" "testing" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" ) @@ -312,7 +314,7 @@ func generateEvent(t *testing.T, aggregateID string, opts ...func(*repository.Ev ResourceOwner: sql.NullString{String: "ro", Valid: true}, Typ: "test.created", Version: "v1", - Pos: 42, + Pos: decimal.NewFromInt(42), } for _, opt := range opts { diff --git a/internal/eventstore/repository/sql/query.go b/internal/eventstore/repository/sql/query.go index a545225d9e..8584a82fa0 100644 --- a/internal/eventstore/repository/sql/query.go +++ b/internal/eventstore/repository/sql/query.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/database" @@ -24,7 +25,7 @@ type querier interface { conditionFormat(repository.Operation) string placeholder(query string) string eventQuery(useV1 bool) string - maxSequenceQuery(useV1 bool) string + maxPositionQuery(useV1 bool) string instanceIDsQuery(useV1 bool) string Client() *database.DB orderByEventSequence(desc, shouldOrderBySequence, useV1 bool) string @@ -68,7 +69,7 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search // instead of using the max function of the database (which doesn't work for postgres) // we select the most recent row - if q.Columns == eventstore.ColumnsMaxSequence { + if q.Columns == eventstore.ColumnsMaxPosition { q.Limit = 1 q.Desc = true } @@ -85,7 +86,7 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search switch q.Columns { case eventstore.ColumnsEvent, - eventstore.ColumnsMaxSequence: + eventstore.ColumnsMaxPosition: query += criteria.orderByEventSequence(q.Desc, shouldOrderBySequence, useV1) } @@ -141,8 +142,8 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search func prepareColumns(criteria querier, columns eventstore.Columns, useV1 bool) (string, func(s scan, dest interface{}) error) { switch columns { - case eventstore.ColumnsMaxSequence: - return criteria.maxSequenceQuery(useV1), maxSequenceScanner + case eventstore.ColumnsMaxPosition: + return criteria.maxPositionQuery(useV1), maxPositionScanner case eventstore.ColumnsInstanceIDs: return criteria.instanceIDsQuery(useV1), instanceIDsScanner case eventstore.ColumnsEvent: @@ -152,13 +153,15 @@ func prepareColumns(criteria querier, columns eventstore.Columns, useV1 bool) (s } } -func maxSequenceScanner(row scan, dest any) (err error) { - position, ok := dest.(*sql.NullFloat64) +func maxPositionScanner(row scan, dest interface{}) (err error) { + position, ok := dest.(*decimal.Decimal) if !ok { - return zerrors.ThrowInvalidArgumentf(nil, "SQL-NBjA9", "type must be sql.NullInt64 got: %T", dest) + return zerrors.ThrowInvalidArgumentf(nil, "SQL-NBjA9", "type must be pointer to decimal.Decimal got: %T", dest) } - err = row(position) + var res decimal.NullDecimal + err = row(&res) if err == nil || errors.Is(err, sql.ErrNoRows) { + *position = res.Decimal return nil } return zerrors.ThrowInternal(err, "SQL-bN5xg", "something went wrong") @@ -187,7 +190,7 @@ func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error) return zerrors.ThrowInvalidArgumentf(nil, "SQL-4GP6F", "events scanner: invalid type %T", dest) } event := new(repository.Event) - position := new(sql.NullFloat64) + position := new(decimal.NullDecimal) if useV1 { err = scanner( @@ -224,7 +227,7 @@ func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error) logging.New().WithError(err).Warn("unable to scan row") return zerrors.ThrowInternal(err, "SQL-M0dsf", "unable to scan row") } - event.Pos = position.Float64 + event.Pos = position.Decimal return reduce(event) } } diff --git a/internal/eventstore/repository/sql/query_test.go b/internal/eventstore/repository/sql/query_test.go index 3df819be64..0e2425dd07 100644 --- a/internal/eventstore/repository/sql/query_test.go +++ b/internal/eventstore/repository/sql/query_test.go @@ -7,10 +7,12 @@ import ( "reflect" "regexp" "strconv" + "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" + "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/database" @@ -111,36 +113,36 @@ func Test_prepareColumns(t *testing.T) { { name: "max column", args: args{ - columns: eventstore.ColumnsMaxSequence, - dest: new(sql.NullFloat64), + columns: eventstore.ColumnsMaxPosition, + dest: new(decimal.Decimal), useV1: true, }, res: res{ query: `SELECT event_sequence FROM eventstore.events`, - expected: sql.NullFloat64{Float64: 43, Valid: true}, + expected: decimal.NewFromInt(42), }, fields: fields{ - dbRow: []interface{}{sql.NullFloat64{Float64: 43, Valid: true}}, + dbRow: []interface{}{decimal.NewNullDecimal(decimal.NewFromInt(42))}, }, }, { name: "max column v2", args: args{ - columns: eventstore.ColumnsMaxSequence, - dest: new(sql.NullFloat64), + columns: eventstore.ColumnsMaxPosition, + dest: new(decimal.Decimal), }, res: res{ query: `SELECT "position" FROM eventstore.events2`, - expected: sql.NullFloat64{Float64: 43, Valid: true}, + expected: decimal.NewFromInt(42), }, fields: fields{ - dbRow: []interface{}{sql.NullFloat64{Float64: 43, Valid: true}}, + dbRow: []interface{}{decimal.NewNullDecimal(decimal.NewFromInt(42))}, }, }, { name: "max sequence wrong dest type", args: args{ - columns: eventstore.ColumnsMaxSequence, + columns: eventstore.ColumnsMaxPosition, dest: new(uint64), }, res: res{ @@ -180,11 +182,11 @@ func Test_prepareColumns(t *testing.T) { res: res{ query: `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2`, expected: []eventstore.Event{ - &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: 42, Data: nil, Version: "v1"}, + &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: decimal.NewFromInt(42), Data: nil, Version: "v1"}, }, }, fields: fields{ - dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), sql.NullFloat64{Float64: 42, Valid: true}, sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, + dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), decimal.NewNullDecimal(decimal.NewFromInt(42)), sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, }, }, { @@ -199,11 +201,11 @@ func Test_prepareColumns(t *testing.T) { res: res{ query: `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2`, expected: []eventstore.Event{ - &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: 0, Data: nil, Version: "v1"}, + &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: decimal.Decimal{}, Data: nil, Version: "v1"}, }, }, fields: fields{ - dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), sql.NullFloat64{Float64: 0, Valid: false}, sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, + dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), decimal.NullDecimal{}, sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, }, }, { @@ -901,7 +903,7 @@ func Test_query_events_mocked(t *testing.T) { InstanceID("instanceID"). OrderDesc(). Limit(5). - PositionAfter(123.456). + PositionAtLeast(decimal.NewFromFloat(123.456)). AddQuery(). AggregateTypes("notify"). EventTypes("notify.foo.bar"). @@ -914,8 +916,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ 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)}, + 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"), decimal.NewFromFloat(123.456), eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", decimal.NewFromFloat(123.456), uint64(5)}, ), }, res: res{ @@ -930,7 +932,7 @@ func Test_query_events_mocked(t *testing.T) { InstanceID("instanceID"). OrderDesc(). Limit(5). - PositionAfter(123.456). + PositionAtLeast(decimal.NewFromFloat(123.456)). AddQuery(). AggregateTypes("notify"). EventTypes("notify.foo.bar"). @@ -943,8 +945,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ 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)}, + 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"), decimal.NewFromFloat(123.456), eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", decimal.NewFromFloat(123.456), uint64(5)}, ), }, res: res{ @@ -988,6 +990,10 @@ func Test_query_events_mocked(t *testing.T) { client.DB.DB = tt.fields.mock.client } + if strings.HasPrefix(tt.name, "aggregate / event type, position and exclusion") { + t.Log("hodor") + } + 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) diff --git a/internal/eventstore/search_query.go b/internal/eventstore/search_query.go index 1596936a36..dc92f5a4de 100644 --- a/internal/eventstore/search_query.go +++ b/internal/eventstore/search_query.go @@ -5,6 +5,8 @@ import ( "database/sql" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -25,7 +27,7 @@ type SearchQueryBuilder struct { tx *sql.Tx lockRows bool lockOption LockOption - positionAfter float64 + positionAtLeast decimal.Decimal awaitOpenTransactions bool creationDateAfter time.Time creationDateBefore time.Time @@ -76,8 +78,8 @@ func (b *SearchQueryBuilder) GetTx() *sql.Tx { return b.tx } -func (b SearchQueryBuilder) GetPositionAfter() float64 { - return b.positionAfter +func (b SearchQueryBuilder) GetPositionAtLeast() decimal.Decimal { + return b.positionAtLeast } func (b SearchQueryBuilder) GetAwaitOpenTransactions() bool { @@ -113,7 +115,7 @@ type SearchQuery struct { aggregateIDs []string eventTypes []EventType eventData map[string]interface{} - positionAfter float64 + positionAfter decimal.Decimal } func (q SearchQuery) GetAggregateTypes() []AggregateType { @@ -132,7 +134,7 @@ func (q SearchQuery) GetEventData() map[string]interface{} { return q.eventData } -func (q SearchQuery) GetPositionAfter() float64 { +func (q SearchQuery) GetPositionAfter() decimal.Decimal { return q.positionAfter } @@ -156,8 +158,8 @@ type Columns int8 const ( //ColumnsEvent represents all fields of an event ColumnsEvent = iota + 1 - // ColumnsMaxSequence represents the latest sequence of the filtered events - ColumnsMaxSequence + // ColumnsMaxPosition represents the latest sequence of the filtered events + ColumnsMaxPosition // ColumnsInstanceIDs represents the instance ids of the filtered events ColumnsInstanceIDs @@ -284,9 +286,9 @@ func (builder *SearchQueryBuilder) EditorUser(id string) *SearchQueryBuilder { return builder } -// PositionAfter filters for events which happened after the specified time -func (builder *SearchQueryBuilder) PositionAfter(position float64) *SearchQueryBuilder { - builder.positionAfter = position +// PositionAtLeast filters for events which happened after the specified time +func (builder *SearchQueryBuilder) PositionAtLeast(position decimal.Decimal) *SearchQueryBuilder { + builder.positionAtLeast = position return builder } @@ -393,7 +395,7 @@ func (query *SearchQuery) EventData(data map[string]interface{}) *SearchQuery { return query } -func (query *SearchQuery) PositionAfter(position float64) *SearchQuery { +func (query *SearchQuery) PositionAfter(position decimal.Decimal) *SearchQuery { query.positionAfter = position return query } diff --git a/internal/eventstore/search_query_test.go b/internal/eventstore/search_query_test.go index b8f570dc0d..3325ee0c4b 100644 --- a/internal/eventstore/search_query_test.go +++ b/internal/eventstore/search_query_test.go @@ -106,10 +106,10 @@ func TestSearchQuerybuilderSetters(t *testing.T) { { name: "set columns", args: args{ - setters: []func(*SearchQueryBuilder) *SearchQueryBuilder{testSetColumns(ColumnsMaxSequence)}, + setters: []func(*SearchQueryBuilder) *SearchQueryBuilder{testSetColumns(ColumnsMaxPosition)}, }, res: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, }, }, { diff --git a/internal/eventstore/v1/models/event.go b/internal/eventstore/v1/models/event.go index 8c50d64da0..ab2b608872 100644 --- a/internal/eventstore/v1/models/event.go +++ b/internal/eventstore/v1/models/event.go @@ -5,6 +5,8 @@ import ( "reflect" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -20,7 +22,7 @@ var _ eventstore.Event = (*Event)(nil) type Event struct { ID string Seq uint64 - Pos float64 + Pos decimal.Decimal CreationDate time.Time Typ eventstore.EventType PreviousSequence uint64 @@ -80,7 +82,7 @@ func (e *Event) Sequence() uint64 { } // Position implements [eventstore.Event] -func (e *Event) Position() float64 { +func (e *Event) Position() decimal.Decimal { return e.Pos } diff --git a/internal/eventstore/v3/event.go b/internal/eventstore/v3/event.go index 1141a9eacf..c9ea4d2c62 100644 --- a/internal/eventstore/v3/event.go +++ b/internal/eventstore/v3/event.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -42,7 +43,7 @@ type event struct { command *command createdAt time.Time sequence uint64 - position float64 + position decimal.Decimal } // TODO: remove on v3 @@ -152,8 +153,8 @@ func (e *event) Sequence() uint64 { return e.sequence } -// Sequence implements [eventstore.Event] -func (e *event) Position() float64 { +// Position implements [eventstore.Event] +func (e *event) Position() decimal.Decimal { return e.position } diff --git a/internal/eventstore/write_model.go b/internal/eventstore/write_model.go index 277e65ed82..965fb16d0e 100644 --- a/internal/eventstore/write_model.go +++ b/internal/eventstore/write_model.go @@ -1,6 +1,8 @@ package eventstore -import "time" +import ( + "time" +) // WriteModel is the minimum representation of a command side write model. // It implements a basic reducer @@ -27,21 +29,25 @@ func (wm *WriteModel) Reduce() error { return nil } + latestEvent := wm.Events[len(wm.Events)-1] if wm.AggregateID == "" { - wm.AggregateID = wm.Events[0].Aggregate().ID - } - if wm.ResourceOwner == "" { - wm.ResourceOwner = wm.Events[0].Aggregate().ResourceOwner - } - if wm.InstanceID == "" { - wm.InstanceID = wm.Events[0].Aggregate().InstanceID + wm.AggregateID = latestEvent.Aggregate().ID } - wm.ProcessedSequence = wm.Events[len(wm.Events)-1].Sequence() - wm.ChangeDate = wm.Events[len(wm.Events)-1].CreatedAt() + if wm.ResourceOwner == "" { + wm.ResourceOwner = latestEvent.Aggregate().ResourceOwner + } + + if wm.InstanceID == "" { + wm.InstanceID = latestEvent.Aggregate().InstanceID + } + + wm.ProcessedSequence = latestEvent.Sequence() + wm.ChangeDate = latestEvent.CreatedAt() // all events processed and not needed anymore wm.Events = nil wm.Events = []Event{} + return nil } diff --git a/internal/execution/handlers.go b/internal/execution/handlers.go index 7ffb4cc6ff..030e6d5186 100644 --- a/internal/execution/handlers.go +++ b/internal/execution/handlers.go @@ -84,6 +84,9 @@ func (u *eventHandler) Reducers() []handler.AggregateReducer { return aggReducers } +// FilterGlobalEvents implements [handler.GlobalProjection] +func (u *eventHandler) FilterGlobalEvents() {} + func groupsFromEventType(s string) []string { parts := strings.Split(s, ".") groups := make([]string, len(parts)) diff --git a/internal/feature/feature.go b/internal/feature/feature.go index 389b750483..b5f5a901d4 100644 --- a/internal/feature/feature.go +++ b/internal/feature/feature.go @@ -57,10 +57,12 @@ type Features struct { ConsoleUseV2UserApi bool `json:"console_use_v2_user_api,omitempty"` } +/* Note: do not generate the stringer or enumer for this type, is it breaks existing events */ + type ImprovedPerformanceType int32 const ( - ImprovedPerformanceTypeUnknown = iota + ImprovedPerformanceTypeUnspecified ImprovedPerformanceType = iota ImprovedPerformanceTypeOrgByID ImprovedPerformanceTypeProjectGrant ImprovedPerformanceTypeProject diff --git a/internal/feature/key_enumer.go b/internal/feature/key_enumer.go index 6466061718..a47b3eb4d9 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_v2console_use_v2_user_api" +const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactions_deprecatedimproved_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, 297} +var _KeyIndex = [...]uint16{0, 11, 28, 61, 81, 92, 106, 124, 144, 151, 174, 208, 232, 258, 266, 285, 308} -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" +const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactions_deprecatedimproved_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) { @@ -57,26 +57,26 @@ var _KeyNameToValueMap = map[string]Key{ _KeyLowerName[81:92]: KeyUserSchema, _KeyName[92:106]: KeyTokenExchange, _KeyLowerName[92:106]: KeyTokenExchange, - _KeyName[106:113]: KeyActionsDeprecated, - _KeyLowerName[106:113]: KeyActionsDeprecated, - _KeyName[113:133]: KeyImprovedPerformance, - _KeyLowerName[113:133]: KeyImprovedPerformance, - _KeyName[133:140]: KeyWebKey, - _KeyLowerName[133:140]: KeyWebKey, - _KeyName[140:163]: KeyDebugOIDCParentError, - _KeyLowerName[140:163]: KeyDebugOIDCParentError, - _KeyName[163:197]: KeyOIDCSingleV1SessionTermination, - _KeyLowerName[163:197]: KeyOIDCSingleV1SessionTermination, - _KeyName[197:221]: KeyDisableUserTokenEvent, - _KeyLowerName[197:221]: KeyDisableUserTokenEvent, - _KeyName[221:247]: KeyEnableBackChannelLogout, - _KeyLowerName[221:247]: KeyEnableBackChannelLogout, - _KeyName[247:255]: KeyLoginV2, - _KeyLowerName[247:255]: KeyLoginV2, - _KeyName[255:274]: KeyPermissionCheckV2, - _KeyLowerName[255:274]: KeyPermissionCheckV2, - _KeyName[274:297]: KeyConsoleUseV2UserApi, - _KeyLowerName[274:297]: KeyConsoleUseV2UserApi, + _KeyName[106:124]: KeyActionsDeprecated, + _KeyLowerName[106:124]: KeyActionsDeprecated, + _KeyName[124:144]: KeyImprovedPerformance, + _KeyLowerName[124:144]: KeyImprovedPerformance, + _KeyName[144:151]: KeyWebKey, + _KeyLowerName[144:151]: KeyWebKey, + _KeyName[151:174]: KeyDebugOIDCParentError, + _KeyLowerName[151:174]: KeyDebugOIDCParentError, + _KeyName[174:208]: KeyOIDCSingleV1SessionTermination, + _KeyLowerName[174:208]: KeyOIDCSingleV1SessionTermination, + _KeyName[208:232]: KeyDisableUserTokenEvent, + _KeyLowerName[208:232]: KeyDisableUserTokenEvent, + _KeyName[232:258]: KeyEnableBackChannelLogout, + _KeyLowerName[232:258]: KeyEnableBackChannelLogout, + _KeyName[258:266]: KeyLoginV2, + _KeyLowerName[258:266]: KeyLoginV2, + _KeyName[266:285]: KeyPermissionCheckV2, + _KeyLowerName[266:285]: KeyPermissionCheckV2, + _KeyName[285:308]: KeyConsoleUseV2UserApi, + _KeyLowerName[285:308]: KeyConsoleUseV2UserApi, } var _KeyNames = []string{ @@ -86,16 +86,16 @@ var _KeyNames = []string{ _KeyName[61:81], _KeyName[81:92], _KeyName[92:106], - _KeyName[106:113], - _KeyName[113:133], - _KeyName[133:140], - _KeyName[140:163], - _KeyName[163:197], - _KeyName[197:221], - _KeyName[221:247], - _KeyName[247:255], - _KeyName[255:274], - _KeyName[274:297], + _KeyName[106:124], + _KeyName[124:144], + _KeyName[144:151], + _KeyName[151:174], + _KeyName[174:208], + _KeyName[208:232], + _KeyName[232:258], + _KeyName[258:266], + _KeyName[266:285], + _KeyName[285:308], } // KeyString retrieves an enum value from the enum constants string name. diff --git a/internal/idp/providers/apple/session.go b/internal/idp/providers/apple/session.go index eee68fa2a5..99794d18a2 100644 --- a/internal/idp/providers/apple/session.go +++ b/internal/idp/providers/apple/session.go @@ -10,6 +10,8 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/oidc" ) +var _ idp.Session = (*Session)(nil) + // Session extends the [oidc.Session] with the formValues returned from the callback. // This enables to parse the user (name and email), which Apple only returns as form params on registration type Session struct { @@ -58,6 +60,10 @@ func NewUser(info *openid.UserInfo, names userNamesFormValue) *User { return &User{User: user} } +func InitUser() idp.User { + return &User{User: oidc.InitUser()} +} + // User extends the [oidc.User] by returning the email as preferred_username, since Apple does not return the latter. type User struct { *oidc.User diff --git a/internal/idp/providers/azuread/session.go b/internal/idp/providers/azuread/session.go index 4b0a6fb844..169784fb58 100644 --- a/internal/idp/providers/azuread/session.go +++ b/internal/idp/providers/azuread/session.go @@ -3,6 +3,7 @@ package azuread import ( "context" "net/http" + "time" "github.com/zitadel/oidc/v3/pkg/client/rp" httphelper "github.com/zitadel/oidc/v3/pkg/http" @@ -12,6 +13,8 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/oauth" ) +var _ idp.Session = (*Session)(nil) + // Session extends the [oauth.Session] to be able to handle the id_token and to implement the [idp.SessionSupportsMigration] functionality type Session struct { *Provider @@ -79,6 +82,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return user, nil } +func (s *Session) ExpiresAt() time.Time { + if s.OAuthSession == nil { + return time.Time{} + } + return s.OAuthSession.ExpiresAt() +} + // Tokens returns the [oidc.Tokens] of the underlying [oauth.Session]. func (s *Session) Tokens() *oidc.Tokens[*oidc.IDTokenClaims] { return s.oauth().Tokens diff --git a/internal/idp/providers/google/google.go b/internal/idp/providers/google/google.go index 221f2b61ae..083d4aef62 100644 --- a/internal/idp/providers/google/google.go +++ b/internal/idp/providers/google/google.go @@ -34,6 +34,10 @@ var userMapper = func(info *openid.UserInfo) idp.User { return &User{oidc.DefaultMapper(info)} } +func InitUser() idp.User { + return &User{oidc.InitUser()} +} + // User is a representation of the authenticated Google and implements the [idp.User] interface // by wrapping an [idp.User] (implemented by [oidc.User]). It overwrites the [GetPreferredUsername] to use the `email` claim. type User struct { diff --git a/internal/idp/providers/jwt/jwt.go b/internal/idp/providers/jwt/jwt.go index 99347f31a3..d972102b01 100644 --- a/internal/idp/providers/jwt/jwt.go +++ b/internal/idp/providers/jwt/jwt.go @@ -11,14 +11,14 @@ import ( ) const ( - queryAuthRequestID = "authRequestID" - queryUserAgentID = "userAgentID" + QueryAuthRequestID = "authRequestID" + QueryUserAgentID = "userAgentID" ) var _ idp.Provider = (*Provider)(nil) var ( - ErrMissingUserAgentID = errors.New("userAgentID missing") + ErrMissingState = errors.New("state missing") ) // Provider is the [idp.Provider] implementation for a JWT provider @@ -92,32 +92,32 @@ func (p *Provider) Name() string { // It will create a [Session] with an AuthURL, pointing to the jwtEndpoint // with the authRequest and encrypted userAgent ids. func (p *Provider) BeginAuth(ctx context.Context, state string, params ...idp.Parameter) (idp.Session, error) { - userAgentID, err := userAgentIDFromParams(params...) - if err != nil { - return nil, err + if state == "" { + return nil, ErrMissingState } + userAgentID := userAgentIDFromParams(state, params...) redirect, err := url.Parse(p.jwtEndpoint) if err != nil { return nil, err } q := redirect.Query() - q.Set(queryAuthRequestID, state) + q.Set(QueryAuthRequestID, state) nonce, err := p.encryptionAlg.Encrypt([]byte(userAgentID)) if err != nil { return nil, err } - q.Set(queryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce)) + q.Set(QueryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce)) redirect.RawQuery = q.Encode() return &Session{AuthURL: redirect.String()}, nil } -func userAgentIDFromParams(params ...idp.Parameter) (string, error) { +func userAgentIDFromParams(state string, params ...idp.Parameter) string { for _, param := range params { if id, ok := param.(idp.UserAgentID); ok { - return string(id), nil + return string(id) } } - return "", ErrMissingUserAgentID + return state } // IsLinkingAllowed implements the [idp.Provider] interface. diff --git a/internal/idp/providers/jwt/jwt_test.go b/internal/idp/providers/jwt/jwt_test.go index 59e32b4690..5756c58e07 100644 --- a/internal/idp/providers/jwt/jwt_test.go +++ b/internal/idp/providers/jwt/jwt_test.go @@ -23,6 +23,7 @@ func TestProvider_BeginAuth(t *testing.T) { encryptionAlg func(t *testing.T) crypto.EncryptionAlgorithm } type args struct { + state string params []idp.Parameter } type want struct { @@ -36,7 +37,7 @@ func TestProvider_BeginAuth(t *testing.T) { want want }{ { - name: "missing userAgentID error", + name: "missing state, error", fields: fields{ issuer: "https://jwt.com", jwtEndpoint: "https://auth.com/jwt", @@ -47,14 +48,34 @@ func TestProvider_BeginAuth(t *testing.T) { }, }, args: args{ + state: "", params: nil, }, want: want{ err: func(err error) bool { - return errors.Is(err, ErrMissingUserAgentID) + return errors.Is(err, ErrMissingState) }, }, }, + { + name: "missing userAgentID, fallback to state", + fields: fields{ + issuer: "https://jwt.com", + jwtEndpoint: "https://auth.com/jwt", + keysEndpoint: "https://jwt.com/keys", + headerName: "jwt-header", + encryptionAlg: func(t *testing.T) crypto.EncryptionAlgorithm { + return crypto.CreateMockEncryptionAlg(gomock.NewController(t)) + }, + }, + args: args{ + state: "testState", + params: nil, + }, + want: want{ + session: &Session{AuthURL: "https://auth.com/jwt?authRequestID=testState&userAgentID=dGVzdFN0YXRl"}, + }, + }, { name: "successful auth", fields: fields{ @@ -67,6 +88,7 @@ func TestProvider_BeginAuth(t *testing.T) { }, }, args: args{ + state: "testState", params: []idp.Parameter{ idp.UserAgentID("agent"), }, @@ -91,7 +113,7 @@ func TestProvider_BeginAuth(t *testing.T) { require.NoError(t, err) ctx := context.Background() - session, err := provider.BeginAuth(ctx, "testState", tt.args.params...) + session, err := provider.BeginAuth(ctx, tt.args.state, tt.args.params...) if tt.want.err != nil && !tt.want.err(err) { a.Fail("invalid error", err) } diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index 6df08a6998..85b164a9c5 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -5,11 +5,13 @@ import ( "errors" "fmt" "net/http" + "strings" "time" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/oidc" + "golang.org/x/oauth2" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" @@ -34,6 +36,11 @@ func NewSession(provider *Provider, tokens *oidc.Tokens[*oidc.IDTokenClaims]) *S return &Session{Provider: provider, Tokens: tokens} } +func NewSessionFromRequest(provider *Provider, r *http.Request) *Session { + token := strings.TrimPrefix(r.Header.Get(provider.headerName), oidc.PrefixBearer) + return NewSession(provider, &oidc.Tokens[*oidc.IDTokenClaims]{IDToken: token, Token: &oauth2.Token{}}) +} + // GetAuth implements the [idp.Session] interface. func (s *Session) GetAuth(ctx context.Context) (string, bool) { return idp.Redirect(s.AuthURL) @@ -57,6 +64,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return &User{s.Tokens.IDTokenClaims}, nil } +func (s *Session) ExpiresAt() time.Time { + if s.Tokens == nil || s.Tokens.IDTokenClaims == nil { + return time.Time{} + } + return s.Tokens.IDTokenClaims.GetExpiration() +} + func (s *Session) validateToken(ctx context.Context, token string) (*oidc.IDTokenClaims, error) { logging.Debug("begin token validation") // TODO: be able to specify them in the template: https://github.com/zitadel/zitadel/issues/5322 @@ -92,6 +106,12 @@ func (s *Session) validateToken(ctx context.Context, token string) (*oidc.IDToke return claims, nil } +func InitUser() *User { + return &User{ + IDTokenClaims: &oidc.IDTokenClaims{}, + } +} + type User struct { *oidc.IDTokenClaims } diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go index 0a6a87ba3d..a78dd02d73 100644 --- a/internal/idp/providers/ldap/session.go +++ b/internal/idp/providers/ldap/session.go @@ -96,6 +96,10 @@ func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) { ) } +func (s *Session) ExpiresAt() time.Time { + return time.Time{} // falls back to the default expiration time +} + func tryBind( server string, startTLS bool, @@ -129,7 +133,6 @@ func tryBind( username, password, timeout, - rootCA, ) } @@ -185,12 +188,11 @@ func trySearchAndUserBind( username string, password string, timeout time.Duration, - rootCA []byte, ) (*ldap.Entry, error) { searchQuery := queriesAndToSearchQuery( objectClassesToSearchQuery(objectClasses), queriesOrToSearchQuery( - userFiltersToSearchQuery(userFilters, username), + userFiltersToSearchQuery(userFilters, username)..., ), ) @@ -214,7 +216,12 @@ func trySearchAndUserBind( user := sr.Entries[0] // Bind as the user to verify their password - if err = conn.Bind(user.DN, password); err != nil { + userDN, err := ldap.ParseDN(user.DN) + if err != nil { + logging.WithFields("userDN", user.DN).WithError(err).Info("ldap user parse DN failed") + return nil, err + } + if err = conn.Bind(userDN.String(), password); err != nil { logging.WithFields("userDN", user.DN).WithError(err).Info("ldap user bind failed") return nil, ErrFailedLogin } @@ -257,12 +264,12 @@ func objectClassesToSearchQuery(classes []string) string { return searchQuery } -func userFiltersToSearchQuery(filters []string, username string) string { - searchQuery := "" - for _, filter := range filters { - searchQuery += "(" + filter + "=" + ldap.EscapeFilter(username) + ")" +func userFiltersToSearchQuery(filters []string, username string) []string { + searchQueries := make([]string, len(filters)) + for i, filter := range filters { + searchQueries[i] = "(" + filter + "=" + username + ")" } - return searchQuery + return searchQueries } func mapLDAPEntryToUser( diff --git a/internal/idp/providers/ldap/session_test.go b/internal/idp/providers/ldap/session_test.go index 69ba3a3256..89fee68718 100644 --- a/internal/idp/providers/ldap/session_test.go +++ b/internal/idp/providers/ldap/session_test.go @@ -49,31 +49,31 @@ func TestProvider_userFiltersToSearchQuery(t *testing.T) { name string fields []string username string - want string + want []string }{ { name: "zero", fields: []string{}, username: "user", - want: "", + want: []string{}, }, { name: "one", fields: []string{"test"}, username: "user", - want: "(test=user)", + want: []string{"(test=user)"}, }, { name: "three", fields: []string{"test1", "test2", "test3"}, username: "user", - want: "(test1=user)(test2=user)(test3=user)", + want: []string{"(test1=user)", "(test2=user)", "(test3=user)"}, }, { name: "five", fields: []string{"test1", "test2", "test3", "test4", "test5"}, username: "user", - want: "(test1=user)(test2=user)(test3=user)(test4=user)(test5=user)", + want: []string{"(test1=user)", "(test2=user)", "(test3=user)", "(test4=user)", "(test5=user)"}, }, } for _, tt := range tests { diff --git a/internal/idp/providers/oauth/session.go b/internal/idp/providers/oauth/session.go index 247a7f8710..c9e175d1cf 100644 --- a/internal/idp/providers/oauth/session.go +++ b/internal/idp/providers/oauth/session.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + "time" "github.com/zitadel/oidc/v3/pkg/client/rp" httphelper "github.com/zitadel/oidc/v3/pkg/http" @@ -69,6 +70,13 @@ func (s *Session) FetchUser(ctx context.Context) (_ idp.User, err error) { return user, nil } +func (s *Session) ExpiresAt() time.Time { + if s.Tokens == nil { + return time.Time{} + } + return s.Tokens.Expiry +} + func (s *Session) authorize(ctx context.Context) (err error) { if s.Code == "" { return ErrCodeMissing diff --git a/internal/idp/providers/oidc/session.go b/internal/idp/providers/oidc/session.go index b17a3b0a0b..9e1e55baf5 100644 --- a/internal/idp/providers/oidc/session.go +++ b/internal/idp/providers/oidc/session.go @@ -3,6 +3,7 @@ package oidc import ( "context" "errors" + "time" "github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -72,6 +73,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return u, nil } +func (s *Session) ExpiresAt() time.Time { + if s.Tokens == nil { + return time.Time{} + } + return s.Tokens.Expiry +} + func (s *Session) Authorize(ctx context.Context) (err error) { if s.Code == "" { return ErrCodeMissing @@ -88,6 +96,10 @@ func NewUser(info *oidc.UserInfo) *User { return &User{UserInfo: info} } +func InitUser() *User { + return &User{UserInfo: &oidc.UserInfo{}} +} + type User struct { *oidc.UserInfo } diff --git a/internal/idp/providers/saml/session.go b/internal/idp/providers/saml/session.go index b0748d33a3..e2a1655a26 100644 --- a/internal/idp/providers/saml/session.go +++ b/internal/idp/providers/saml/session.go @@ -6,6 +6,7 @@ import ( "errors" "net/http" "net/url" + "time" "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" @@ -107,6 +108,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return userMapper, nil } +func (s *Session) ExpiresAt() time.Time { + if s.Assertion == nil || s.Assertion.Conditions == nil { + return time.Time{} + } + return s.Assertion.Conditions.NotOnOrAfter +} + func (s *Session) transientMappingID() (string, error) { for _, statement := range s.Assertion.AttributeStatements { for _, attribute := range statement.Attributes { diff --git a/internal/idp/session.go b/internal/idp/session.go index ab54bcabaa..fc593eb820 100644 --- a/internal/idp/session.go +++ b/internal/idp/session.go @@ -2,6 +2,7 @@ package idp import ( "context" + "time" ) // Session is the minimal implementation for a session of a 3rd party authentication [Provider] @@ -9,6 +10,7 @@ type Session interface { GetAuth(ctx context.Context) (content string, redirect bool) PersistentParameters() map[string]any FetchUser(ctx context.Context) (User, error) + ExpiresAt() time.Time } // SessionSupportsMigration is an optional extension to the Session interface. diff --git a/internal/integration/client.go b/internal/integration/client.go index e82a6bec55..20c98b5628 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -16,6 +16,7 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration/scim" @@ -26,6 +27,7 @@ import ( feature_v2beta "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" "github.com/zitadel/zitadel/pkg/grpc/idp" idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/object/v2" object_v3alpha "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" @@ -33,6 +35,7 @@ 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" + project_v2beta "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" @@ -70,6 +73,12 @@ type Client struct { UserV3Alpha user_v3alpha.ZITADELUsersClient SAMLv2 saml_pb.SAMLServiceClient SCIM *scim.Client + Projectv2Beta project_v2beta.ProjectServiceClient + InstanceV2Beta instance.InstanceServiceClient +} + +func NewDefaultClient(ctx context.Context) (*Client, error) { + return newClient(ctx, loadedConfig.Host()) } func newClient(ctx context.Context, target string) (*Client, error) { @@ -103,6 +112,8 @@ func newClient(ctx context.Context, target string) (*Client, error) { UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc), SAMLv2: saml_pb.NewSAMLServiceClient(cc), SCIM: scim.NewScimClient(target), + Projectv2Beta: project_v2beta.NewProjectServiceClient(cc), + InstanceV2Beta: instance.NewInstanceServiceClient(cc), } return client, client.pollHealth(ctx) } @@ -131,6 +142,7 @@ func (c *Client) pollHealth(ctx context.Context) (err error) { } } +// Deprecated: use CreateUserTypeHuman instead func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ @@ -162,6 +174,7 @@ func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserRes return resp } +// Deprecated: user CreateUserTypeHuman instead func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ @@ -187,6 +200,7 @@ func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHuman return resp } +// Deprecated: user CreateUserTypeHuman instead func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ @@ -219,6 +233,52 @@ func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) * return resp } +func (i *Instance) CreateUserTypeHuman(ctx context.Context) *user_v2.CreateUserResponse { + resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{ + OrganizationId: i.DefaultOrg.GetId(), + UserType: &user_v2.CreateUserRequest_Human_{ + Human: &user_v2.CreateUserRequest_Human{ + Profile: &user_v2.SetHumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + }, + Email: &user_v2.SetHumanEmail{ + Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), + Verification: &user_v2.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }) + logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetId()) + return resp +} + +func (i *Instance) CreateUserTypeMachine(ctx context.Context) *user_v2.CreateUserResponse { + resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{ + OrganizationId: i.DefaultOrg.GetId(), + UserType: &user_v2.CreateUserRequest_Machine_{ + Machine: &user_v2.CreateUserRequest_Machine{ + Name: "machine", + }, + }, + }) + logging.OnError(err).Panic("create machine user") + i.TriggerUserByID(ctx, resp.GetId()) + return resp +} + +func (i *Instance) CreatePersonalAccessToken(ctx context.Context, userID string) *user_v2.AddPersonalAccessTokenResponse { + resp, err := i.Client.UserV2.AddPersonalAccessToken(ctx, &user_v2.AddPersonalAccessTokenRequest{ + UserId: userID, + ExpirationDate: timestamppb.New(time.Now().Add(30 * time.Minute)), + }) + logging.OnError(err).Panic("create pat") + 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 @@ -286,6 +346,15 @@ func SetOrgID(ctx context.Context, orgID string) context.Context { return metadata.NewOutgoingContext(ctx, md) } +func (i *Instance) CreateOrganizationWithCustomOrgID(ctx context.Context, name, orgID string) *org.AddOrganizationResponse { + resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ + Name: name, + OrgId: gu.Ptr(orgID), + }) + logging.OnError(err).Fatal("create org") + return resp +} + func (i *Instance) CreateOrganizationWithUserID(ctx context.Context, name, userID string) *org.AddOrganizationResponse { resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ Name: name, @@ -430,6 +499,70 @@ func (i *Instance) SetUserPassword(ctx context.Context, userID, password string, return resp.GetDetails() } +func (i *Instance) CreateProject(ctx context.Context, t *testing.T, orgID, name string, projectRoleCheck, hasProjectCheck bool) *project_v2beta.CreateProjectResponse { + if orgID == "" { + orgID = i.DefaultOrg.GetId() + } + + resp, err := i.Client.Projectv2Beta.CreateProject(ctx, &project_v2beta.CreateProjectRequest{ + OrganizationId: orgID, + Name: name, + AuthorizationRequired: projectRoleCheck, + ProjectAccessRequired: hasProjectCheck, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteProject(ctx context.Context, t *testing.T, projectID string) *project_v2beta.DeleteProjectResponse { + resp, err := i.Client.Projectv2Beta.DeleteProject(ctx, &project_v2beta.DeleteProjectRequest{ + Id: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeactivateProject(ctx context.Context, t *testing.T, projectID string) *project_v2beta.DeactivateProjectResponse { + resp, err := i.Client.Projectv2Beta.DeactivateProject(ctx, &project_v2beta.DeactivateProjectRequest{ + Id: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) ActivateProject(ctx context.Context, t *testing.T, projectID string) *project_v2beta.ActivateProjectResponse { + resp, err := i.Client.Projectv2Beta.ActivateProject(ctx, &project_v2beta.ActivateProjectRequest{ + Id: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) AddProjectRole(ctx context.Context, t *testing.T, projectID, roleKey, displayName, group string) *project_v2beta.AddProjectRoleResponse { + var groupP *string + if group != "" { + groupP = &group + } + + resp, err := i.Client.Projectv2Beta.AddProjectRole(ctx, &project_v2beta.AddProjectRoleRequest{ + ProjectId: projectID, + RoleKey: roleKey, + DisplayName: displayName, + Group: groupP, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) RemoveProjectRole(ctx context.Context, t *testing.T, projectID, roleKey string) *project_v2beta.RemoveProjectRoleResponse { + resp, err := i.Client.Projectv2Beta.RemoveProjectRole(ctx, &project_v2beta.RemoveProjectRoleRequest{ + ProjectId: projectID, + RoleKey: roleKey, + }) + require.NoError(t, err) + return resp +} + func (i *Instance) AddProviderToDefaultLoginPolicy(ctx context.Context, id string) { _, err := i.Client.Admin.AddIDPToLoginPolicy(ctx, &admin.AddIDPToLoginPolicyRequest{ IdpId: id, @@ -601,6 +734,24 @@ func (i *Instance) AddLDAPProvider(ctx context.Context) string { return resp.GetId() } +func (i *Instance) AddJWTProvider(ctx context.Context) string { + resp, err := i.Client.Admin.AddJWTProvider(ctx, &admin.AddJWTProviderRequest{ + Name: "jwt-idp", + Issuer: "https://example.com", + JwtEndpoint: "https://example.com/jwt", + KeysEndpoint: "https://example.com/keys", + HeaderName: "Authorization", + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }) + logging.OnError(err).Panic("create jwt 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, @@ -672,12 +823,57 @@ func (i *Instance) CreatePasswordSession(t *testing.T, ctx context.Context, user createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime() } -func (i *Instance) CreateProjectGrant(ctx context.Context, projectID, grantedOrgID string) *mgmt.AddProjectGrantResponse { - resp, err := i.Client.Mgmt.AddProjectGrant(ctx, &mgmt.AddProjectGrantRequest{ - GrantedOrgId: grantedOrgID, - ProjectId: projectID, +func (i *Instance) CreateIntentSession(t *testing.T, ctx context.Context, userID, intentID, intentToken string) (id, token string, start, change time.Time) { + createResp, err := i.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{UserId: userID}, + }, + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: intentToken, + }, + }, }) - logging.OnError(err).Panic("create project grant") + require.NoError(t, err) + return createResp.GetSessionId(), createResp.GetSessionToken(), + createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime() +} + +func (i *Instance) CreateProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string, roles ...string) *project_v2beta.CreateProjectGrantResponse { + resp, err := i.Client.Projectv2Beta.CreateProjectGrant(ctx, &project_v2beta.CreateProjectGrantRequest{ + GrantedOrganizationId: grantedOrgID, + ProjectId: projectID, + RoleKeys: roles, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string) *project_v2beta.DeleteProjectGrantResponse { + resp, err := i.Client.Projectv2Beta.DeleteProjectGrant(ctx, &project_v2beta.DeleteProjectGrantRequest{ + GrantedOrganizationId: grantedOrgID, + ProjectId: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeactivateProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string) *project_v2beta.DeactivateProjectGrantResponse { + resp, err := i.Client.Projectv2Beta.DeactivateProjectGrant(ctx, &project_v2beta.DeactivateProjectGrantRequest{ + ProjectId: projectID, + GrantedOrganizationId: grantedOrgID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) ActivateProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string) *project_v2beta.ActivateProjectGrantResponse { + resp, err := i.Client.Projectv2Beta.ActivateProjectGrant(ctx, &project_v2beta.ActivateProjectGrantRequest{ + ProjectId: projectID, + GrantedOrganizationId: grantedOrgID, + }) + require.NoError(t, err) return resp } @@ -717,6 +913,16 @@ func (i *Instance) CreateProjectMembership(t *testing.T, ctx context.Context, pr require.NoError(t, err) } +func (i *Instance) CreateProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) { + _, err := i.Client.Mgmt.AddProjectGrantMember(ctx, &mgmt.AddProjectGrantMemberRequest{ + ProjectId: projectID, + GrantId: grantID, + UserId: userID, + Roles: []string{domain.RoleProjectGrantOwner}, + }) + require.NoError(t, err) +} + func (i *Instance) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { if name == "" { name = gofakeit.Name() diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index 159fcb0119..3323be3f97 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "strings" + "testing" "time" "github.com/brianvoe/gofakeit/v6" @@ -133,13 +134,9 @@ func (i *Instance) CreateOIDCInactivateProjectClient(ctx context.Context, redire return client, err } -func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI string, loginVersion *app.LoginVersion) (*management.AddOIDCAppResponse, error) { - project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ - Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), - }) - if err != nil { - return nil, err - } +func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, t *testing.T, redirectURI string, loginVersion *app.LoginVersion) (*management.AddOIDCAppResponse, error) { + project := i.CreateProject(ctx, t, "", gofakeit.AppName(), false, false) + resp, err := i.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ ProjectId: project.GetId(), Name: fmt.Sprintf("app-%d", time.Now().UnixNano()), @@ -178,30 +175,11 @@ func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI }) } -func (i *Instance) CreateOIDCTokenExchangeClient(ctx context.Context) (client *management.AddOIDCAppResponse, keyData []byte, err error) { - project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ - Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), - }) - if err != nil { - return nil, nil, err - } +func (i *Instance) CreateOIDCTokenExchangeClient(ctx context.Context, t *testing.T) (client *management.AddOIDCAppResponse, keyData []byte, err error) { + project := i.CreateProject(ctx, t, "", gofakeit.AppName(), false, false) return i.CreateOIDCWebClientJWT(ctx, "", "", project.GetId(), app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN) } -func (i *Instance) CreateProject(ctx context.Context) (*management.AddProjectResponse, error) { - return i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ - Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), - }) -} - -func (i *Instance) CreateProjectWithPermissionCheck(ctx context.Context, projectRoleCheck, hasProjectCheck bool) (*management.AddProjectResponse, error) { - return i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ - Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), - HasProjectCheck: hasProjectCheck, - ProjectRoleCheck: projectRoleCheck, - }) -} - func (i *Instance) CreateAPIClientJWT(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) { return i.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{ ProjectId: projectID, diff --git a/internal/integration/sink/server.go b/internal/integration/sink/server.go index 633ebf424f..653c5236d6 100644 --- a/internal/integration/sink/server.go +++ b/internal/integration/sink/server.go @@ -17,6 +17,7 @@ import ( crewjam_saml "github.com/crewjam/saml" "github.com/go-chi/chi/v5" + goldap "github.com/go-ldap/ldap/v3" "github.com/gorilla/websocket" "github.com/sirupsen/logrus" "github.com/zitadel/logging" @@ -26,6 +27,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/idp/providers/jwt" "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" @@ -48,7 +50,7 @@ func CallURL(ch Channel) string { return u.String() } -func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { +func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { u := url.URL{ Scheme: "http", Host: host, @@ -59,6 +61,7 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, IDPID: idpID, IDPUserID: idpUserID, UserID: userID, + Expiry: expiry, }) if err != nil { return "", "", time.Time{}, uint64(0), err @@ -66,7 +69,7 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil } -func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { +func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { u := url.URL{ Scheme: "http", Host: host, @@ -77,6 +80,7 @@ func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string, IDPID: idpID, IDPUserID: idpUserID, UserID: userID, + Expiry: expiry, }) if err != nil { return "", "", time.Time{}, uint64(0), err @@ -84,7 +88,7 @@ func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string, return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil } -func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { +func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { u := url.URL{ Scheme: "http", Host: host, @@ -95,6 +99,7 @@ func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string, IDPID: idpID, IDPUserID: idpUserID, UserID: userID, + Expiry: expiry, }) if err != nil { return "", "", time.Time{}, uint64(0), err @@ -120,6 +125,25 @@ func SuccessfulLDAPIntent(instanceID, idpID, idpUserID, userID string) (string, return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil } +func SuccessfulJWTIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { + u := url.URL{ + Scheme: "http", + Host: host, + Path: successfulIntentJWTPath(), + } + resp, err := callIntent(u.String(), &SuccessfulIntentRequest{ + InstanceID: instanceID, + IDPID: idpID, + IDPUserID: idpUserID, + UserID: userID, + Expiry: expiry, + }) + 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. @@ -141,6 +165,7 @@ func StartServer(commands *command.Commands) (close func()) { router.HandleFunc(successfulIntentOIDCPath(), successfulIntentHandler(commands, createSuccessfulOIDCIntent)) router.HandleFunc(successfulIntentSAMLPath(), successfulIntentHandler(commands, createSuccessfulSAMLIntent)) router.HandleFunc(successfulIntentLDAPPath(), successfulIntentHandler(commands, createSuccessfulLDAPIntent)) + router.HandleFunc(successfulIntentJWTPath(), successfulIntentHandler(commands, createSuccessfulJWTIntent)) } s := &http.Server{ Addr: listenAddr, @@ -191,6 +216,10 @@ func successfulIntentLDAPPath() string { return path.Join(successfulIntentPath(), "/", "ldap") } +func successfulIntentJWTPath() string { + return path.Join(successfulIntentPath(), "/", "jwt") +} + // forwarder handles incoming HTTP requests from ZITADEL and // forwards them to all subscribed web sockets. type forwarder struct { @@ -282,10 +311,11 @@ func readLoop(ws *websocket.Conn) (done chan error) { } type SuccessfulIntentRequest struct { - InstanceID string `json:"instance_id"` - IDPID string `json:"idp_id"` - IDPUserID string `json:"idp_user_id"` - UserID string `json:"user_id"` + InstanceID string `json:"instance_id"` + IDPID string `json:"idp_id"` + IDPUserID string `json:"idp_user_id"` + UserID string `json:"user_id"` + Expiry time.Time `json:"expiry"` } type SuccessfulIntentResponse struct { IntentID string `json:"intent_id"` @@ -376,6 +406,7 @@ func createSuccessfulOAuthIntent(ctx context.Context, cmd *command.Commands, req Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ AccessToken: "accessToken", + Expiry: req.Expiry, }, IDToken: "idToken", }, @@ -407,6 +438,7 @@ func createSuccessfulOIDCIntent(ctx context.Context, cmd *command.Commands, req Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ AccessToken: "accessToken", + Expiry: req.Expiry, }, IDToken: "idToken", }, @@ -431,9 +463,16 @@ func createSuccessfulSAMLIntent(ctx context.Context, cmd *command.Commands, req ID: req.IDPUserID, Attributes: map[string][]string{"attribute1": {"value1"}}, } - assertion := &crewjam_saml.Assertion{ID: "id"} + session := &saml.Session{ + Assertion: &crewjam_saml.Assertion{ + ID: "id", + Conditions: &crewjam_saml.Conditions{ + NotOnOrAfter: req.Expiry, + }, + }, + } - token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, assertion) + token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, session) if err != nil { return nil, err } @@ -465,8 +504,41 @@ func createSuccessfulLDAPIntent(ctx context.Context, cmd *command.Commands, req "", "", ) - attributes := map[string][]string{"id": {req.IDPUserID}, "username": {username}, "language": {lang.String()}} - token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, attributes) + session := &ldap.Session{Entry: &goldap.Entry{ + Attributes: []*goldap.EntryAttribute{ + {Name: "id", Values: []string{req.IDPUserID}}, + {Name: "username", Values: []string{username}}, + {Name: "language", Values: []string{lang.String()}}, + }, + }} + token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, session) + if err != nil { + return nil, err + } + return &SuccessfulIntentResponse{ + intentID, + token, + writeModel.ChangeDate, + writeModel.ProcessedSequence, + }, nil +} + +func createSuccessfulJWTIntent(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 := &jwt.User{ + IDTokenClaims: &oidc.IDTokenClaims{ + TokenClaims: oidc.TokenClaims{ + Subject: req.IDPUserID, + }, + }, + } + session := &jwt.Session{ + Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ + IDToken: "idToken", + }, + } + token, err := cmd.SucceedIDPIntent(ctx, writeModel, idpUser, session, req.UserID) if err != nil { return nil, err } diff --git a/internal/migration/count_trigger.sql b/internal/migration/count_trigger.sql new file mode 100644 index 0000000000..4b521094ab --- /dev/null +++ b/internal/migration/count_trigger.sql @@ -0,0 +1,43 @@ +{{ define "count_trigger" -}} +CREATE OR REPLACE TRIGGER count_{{ .Resource }} + AFTER INSERT OR DELETE + ON {{ .Table }} + FOR EACH ROW + EXECUTE FUNCTION projections.count_resource( + '{{ .ParentType }}', + '{{ .InstanceIDColumn }}', + '{{ .ParentIDColumn }}', + '{{ .Resource }}' + ); + +CREATE OR REPLACE TRIGGER truncate_{{ .Resource }}_counts + AFTER TRUNCATE + ON {{ .Table }} + FOR EACH STATEMENT + EXECUTE FUNCTION projections.delete_table_counts(); + +-- Prevent inserts and deletes while we populate the counts. +LOCK TABLE {{ .Table }} IN SHARE MODE; + +-- Populate the resource counts for the existing data in the table. +INSERT INTO projections.resource_counts( + instance_id, + table_name, + parent_type, + parent_id, + resource_name, + amount +) +SELECT + {{ .InstanceIDColumn }}, + '{{ .Table }}', + '{{ .ParentType }}', + {{ .ParentIDColumn }}, + '{{ .Resource }}', + COUNT(*) AS amount +FROM {{ .Table }} +GROUP BY ({{ .InstanceIDColumn }}, {{ .ParentIDColumn }}) +ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO +UPDATE SET updated_at = now(), amount = EXCLUDED.amount; + +{{- end -}} diff --git a/internal/migration/delete_parent_counts_trigger.sql b/internal/migration/delete_parent_counts_trigger.sql new file mode 100644 index 0000000000..a2e9df6626 --- /dev/null +++ b/internal/migration/delete_parent_counts_trigger.sql @@ -0,0 +1,13 @@ +{{ define "delete_parent_counts_trigger" -}} + +CREATE OR REPLACE TRIGGER delete_parent_counts_trigger + AFTER DELETE + ON {{ .Table }} + FOR EACH ROW + EXECUTE FUNCTION projections.delete_parent_counts( + '{{ .ParentType }}', + '{{ .InstanceIDColumn }}', + '{{ .ParentIDColumn }}' + ); + +{{- end -}} diff --git a/internal/migration/migration.go b/internal/migration/migration.go index a2224340a7..3aeb2f0612 100644 --- a/internal/migration/migration.go +++ b/internal/migration/migration.go @@ -36,7 +36,10 @@ type errCheckerMigration interface { type RepeatableMigration interface { Migration - Check(lastRun map[string]interface{}) bool + + // Check if the migration should be executed again. + // True will repeat the migration, false will not. + Check(lastRun map[string]any) bool } func Migrate(ctx context.Context, es *eventstore.Eventstore, migration Migration) (err error) { diff --git a/internal/migration/trigger.go b/internal/migration/trigger.go new file mode 100644 index 0000000000..bd06afd5c5 --- /dev/null +++ b/internal/migration/trigger.go @@ -0,0 +1,127 @@ +package migration + +import ( + "context" + "embed" + "fmt" + "strings" + "text/template" + + "github.com/mitchellh/mapstructure" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + countTriggerTmpl = "count_trigger" + deleteParentCountsTmpl = "delete_parent_counts_trigger" +) + +var ( + //go:embed *.sql + templateFS embed.FS + templates = template.Must(template.ParseFS(templateFS, "*.sql")) +) + +// CountTrigger registers the existing projections.count_trigger function. +// The trigger than takes care of keeping count of existing +// rows in the source table. +// It also pre-populates the projections.resource_counts table with +// the counts for the given table. +// +// During the population of the resource_counts table, +// the source table is share-locked to prevent concurrent modifications. +// Projection handlers will be halted until the lock is released. +// SELECT statements are not blocked by the lock. +// +// This migration repeats when any of the arguments are changed, +// such as renaming of a projection table. +func CountTrigger( + db *database.DB, + table string, + parentType domain.CountParentType, + instanceIDColumn string, + parentIDColumn string, + resource string, +) RepeatableMigration { + return &triggerMigration{ + triggerConfig: triggerConfig{ + Table: table, + ParentType: parentType.String(), + InstanceIDColumn: instanceIDColumn, + ParentIDColumn: parentIDColumn, + Resource: resource, + }, + db: db, + templateName: countTriggerTmpl, + } +} + +// DeleteParentCountsTrigger +// +// This migration repeats when any of the arguments are changed, +// such as renaming of a projection table. +func DeleteParentCountsTrigger( + db *database.DB, + table string, + parentType domain.CountParentType, + instanceIDColumn string, + parentIDColumn string, + resource string, +) RepeatableMigration { + return &triggerMigration{ + triggerConfig: triggerConfig{ + Table: table, + ParentType: parentType.String(), + InstanceIDColumn: instanceIDColumn, + ParentIDColumn: parentIDColumn, + Resource: resource, + }, + db: db, + templateName: deleteParentCountsTmpl, + } +} + +type triggerMigration struct { + triggerConfig + db *database.DB + templateName string +} + +// String implements [Migration] and [fmt.Stringer]. +func (m *triggerMigration) String() string { + return fmt.Sprintf("repeatable_%s_%s", m.Resource, m.templateName) +} + +// Execute implements [Migration] +func (m *triggerMigration) Execute(ctx context.Context, _ eventstore.Event) error { + var query strings.Builder + err := templates.ExecuteTemplate(&query, m.templateName, m.triggerConfig) + if err != nil { + return fmt.Errorf("%s: execute trigger template: %w", m, err) + } + _, err = m.db.ExecContext(ctx, query.String()) + if err != nil { + return fmt.Errorf("%s: exec trigger query: %w", m, err) + } + return nil +} + +type triggerConfig struct { + Table string `json:"table,omitempty" mapstructure:"table"` + ParentType string `json:"parent_type,omitempty" mapstructure:"parent_type"` + InstanceIDColumn string `json:"instance_id_column,omitempty" mapstructure:"instance_id_column"` + ParentIDColumn string `json:"parent_id_column,omitempty" mapstructure:"parent_id_column"` + Resource string `json:"resource,omitempty" mapstructure:"resource"` +} + +// Check implements [RepeatableMigration]. +func (c *triggerConfig) Check(lastRun map[string]any) bool { + var dst triggerConfig + if err := mapstructure.Decode(lastRun, &dst); err != nil { + panic(err) + } + return dst != *c +} diff --git a/internal/migration/trigger_test.go b/internal/migration/trigger_test.go new file mode 100644 index 0000000000..5799526428 --- /dev/null +++ b/internal/migration/trigger_test.go @@ -0,0 +1,253 @@ +package migration + +import ( + "context" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/database" +) + +const ( + expCountTriggerQuery = `CREATE OR REPLACE TRIGGER count_resource + AFTER INSERT OR DELETE + ON table + FOR EACH ROW + EXECUTE FUNCTION projections.count_resource( + 'instance', + 'instance_id', + 'parent_id', + 'resource' + ); + +CREATE OR REPLACE TRIGGER truncate_resource_counts + AFTER TRUNCATE + ON table + FOR EACH STATEMENT + EXECUTE FUNCTION projections.delete_table_counts(); + +-- Prevent inserts and deletes while we populate the counts. +LOCK TABLE table IN SHARE MODE; + +-- Populate the resource counts for the existing data in the table. +INSERT INTO projections.resource_counts( + instance_id, + table_name, + parent_type, + parent_id, + resource_name, + amount +) +SELECT + instance_id, + 'table', + 'instance', + parent_id, + 'resource', + COUNT(*) AS amount +FROM table +GROUP BY (instance_id, parent_id) +ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO +UPDATE SET updated_at = now(), amount = EXCLUDED.amount;` + + expDeleteParentCountsQuery = `CREATE OR REPLACE TRIGGER delete_parent_counts_trigger + AFTER DELETE + ON table + FOR EACH ROW + EXECUTE FUNCTION projections.delete_parent_counts( + 'instance', + 'instance_id', + 'parent_id' + );` +) + +func Test_triggerMigration_Execute(t *testing.T) { + type fields struct { + triggerConfig triggerConfig + templateName string + } + tests := []struct { + name string + fields fields + expects func(sqlmock.Sqlmock) + wantErr bool + }{ + { + name: "template error", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: "foo", + }, + expects: func(_ sqlmock.Sqlmock) {}, + wantErr: true, + }, + { + name: "db error", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: countTriggerTmpl, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectExec(regexp.QuoteMeta(expCountTriggerQuery)). + WillReturnError(assert.AnError) + }, + wantErr: true, + }, + { + name: "count trigger", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: countTriggerTmpl, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectExec(regexp.QuoteMeta(expCountTriggerQuery)). + WithoutArgs(). + WillReturnResult( + sqlmock.NewResult(1, 1), + ) + }, + }, + { + name: "count trigger", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: deleteParentCountsTmpl, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectExec(regexp.QuoteMeta(expDeleteParentCountsQuery)). + WithoutArgs(). + WillReturnResult( + sqlmock.NewResult(1, 1), + ) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer func() { + err := mock.ExpectationsWereMet() + require.NoError(t, err) + }() + defer db.Close() + tt.expects(mock) + mock.ExpectClose() + + m := &triggerMigration{ + db: &database.DB{ + DB: db, + }, + triggerConfig: tt.fields.triggerConfig, + templateName: tt.fields.templateName, + } + err = m.Execute(context.Background(), nil) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func Test_triggerConfig_Check(t *testing.T) { + type fields struct { + Table string + ParentType string + InstanceIDColumn string + ParentIDColumn string + Resource string + } + type args struct { + lastRun map[string]any + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "should", + fields: fields{ + Table: "users2", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "user", + }, + args: args{ + lastRun: map[string]any{ + "table": "users1", + "parent_type": "instance", + "instance_id_column": "instance_id", + "parent_id_column": "parent_id", + "resource": "user", + }, + }, + want: true, + }, + { + name: "should not", + fields: fields{ + Table: "users1", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "user", + }, + args: args{ + lastRun: map[string]any{ + "table": "users1", + "parent_type": "instance", + "instance_id_column": "instance_id", + "parent_id_column": "parent_id", + "resource": "user", + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &triggerConfig{ + Table: tt.fields.Table, + ParentType: tt.fields.ParentType, + InstanceIDColumn: tt.fields.InstanceIDColumn, + ParentIDColumn: tt.fields.ParentIDColumn, + Resource: tt.fields.Resource, + } + got := c.Check(tt.args.lastRun) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/notification/projections.go b/internal/notification/projections.go index a2d4d4140e..7fedaaf301 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -2,8 +2,12 @@ package notification import ( "context" + "fmt" "time" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" @@ -39,6 +43,9 @@ func Register( queue.ShouldStart() } + // make sure the slice does not contain old values + projections = nil + 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, queue)) @@ -67,12 +74,34 @@ func Start(ctx context.Context) { } } +func SetCurrentState(ctx context.Context, es *eventstore.Eventstore) error { + if len(projections) == 0 { + return nil + } + position, err := es.LatestPosition(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition).InstanceID(authz.GetInstance(ctx).InstanceID()).OrderDesc().Limit(1)) + if err != nil { + return err + } + + for i, projection := range projections { + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("set current state of notification projection") + _, err = projection.Trigger(ctx, handler.WithMinPosition(position)) + if err != nil { + return err + } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("current state of notification projection set") + } + return nil +} + func ProjectInstance(ctx context.Context) error { - for _, projection := range projections { + for i, projection := range projections { + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting notification projection") _, err := projection.Trigger(ctx) if err != nil { return err } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("notification projection done") } return nil } diff --git a/internal/protoc/protoc-gen-authoption/main.go b/internal/protoc/protoc-gen-authoption/main.go index b0e78c6990..1d6ae21b77 100644 --- a/internal/protoc/protoc-gen-authoption/main.go +++ b/internal/protoc/protoc-gen-authoption/main.go @@ -88,7 +88,7 @@ func main() { } // Write the response to stdout, to be picked up by protoc - fmt.Fprintf(os.Stdout, string(out)) + fmt.Fprint(os.Stdout, string(out)) } func loadTemplate(templateData []byte) *template.Template { diff --git a/internal/query/access_token.go b/internal/query/access_token.go index 0fc1bbb369..030ddda473 100644 --- a/internal/query/access_token.go +++ b/internal/query/access_token.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" @@ -140,7 +141,7 @@ func (q *Queries) accessTokenByOIDCSessionAndTokenID(ctx context.Context, oidcSe // checkSessionNotTerminatedAfter checks if a [session.TerminateType] event (or user events leading to a session termination) // occurred after a certain time and will return an error if so. -func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, position float64, fingerprintID string) (err error) { +func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, position decimal.Decimal, fingerprintID string) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -165,7 +166,7 @@ func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, } type sessionTerminatedModel struct { - position float64 + position decimal.Decimal sessionID string userID string fingerPrintID string diff --git a/internal/query/auth_request_test.go b/internal/query/auth_request_test.go index 152a032cd8..12288d18cc 100644 --- a/internal/query/auth_request_test.go +++ b/internal/query/auth_request_test.go @@ -5,7 +5,6 @@ import ( "database/sql" "database/sql/driver" _ "embed" - "fmt" "regexp" "testing" "time" @@ -22,9 +21,7 @@ import ( ) func TestQueries_AuthRequestByID(t *testing.T) { - expQuery := regexp.QuoteMeta(fmt.Sprintf( - authRequestByIDQuery, - )) + expQuery := regexp.QuoteMeta(authRequestByIDQuery) cols := []string{ projection.AuthRequestColumnID, diff --git a/internal/query/authn_key.go b/internal/query/authn_key.go index 8075422e63..ffbe38e7ae 100644 --- a/internal/query/authn_key.go +++ b/internal/query/authn_key.go @@ -5,6 +5,7 @@ import ( "database/sql" _ "embed" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -18,6 +19,14 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +func keysCheckPermission(ctx context.Context, keys *AuthNKeys, permissionCheck domain.PermissionCheck) { + keys.AuthNKeys = slices.DeleteFunc(keys.AuthNKeys, + func(key *AuthNKey) bool { + return userCheckPermission(ctx, key.ResourceOwner, key.AggregateID, permissionCheck) != nil + }, + ) +} + var ( authNKeyTable = table{ name: projection.AuthNKeyTable, @@ -84,6 +93,7 @@ type AuthNKeys struct { type AuthNKey struct { ID string + AggregateID string CreationDate time.Time ChangeDate time.Time ResourceOwner string @@ -124,12 +134,47 @@ func (q *AuthNKeySearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder return query } -func (q *Queries) SearchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, withOwnerRemoved bool) (authNKeys *AuthNKeys, err error) { +type JoinFilter int + +const ( + JoinFilterUnspecified JoinFilter = iota + JoinFilterApp + JoinFilterUserMachine +) + +// SearchAuthNKeys returns machine or app keys, depending on the join filter. +// If permissionCheck is nil, the keys are not filtered. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is false, the returned keys are filtered in-memory by the given permission check. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is true, the returned keys are filtered in the database. +func (q *Queries) SearchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, filter JoinFilter, permissionCheck domain.PermissionCheck) (authNKeys *AuthNKeys, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + keys, err := q.searchAuthNKeys(ctx, queries, filter, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + keysCheckPermission(ctx, keys, permissionCheck) + } + return keys, nil +} + +func (q *Queries) searchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, joinFilter JoinFilter, permissionCheckV2 bool) (authNKeys *AuthNKeys, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareAuthNKeysQuery() query = queries.toQuery(query) + switch joinFilter { + case JoinFilterUnspecified: + // Select all authN keys + case JoinFilterApp: + joinCol := ProjectColumnID + query = query.Join(joinCol.table.identifier() + " ON " + AuthNKeyColumnIdentifier.identifier() + " = " + joinCol.identifier()) + case JoinFilterUserMachine: + joinCol := MachineUserIDCol + query = query.Join(joinCol.table.identifier() + " ON " + AuthNKeyColumnIdentifier.identifier() + " = " + joinCol.identifier()) + query = userPermissionCheckV2WithCustomColumns(ctx, query, permissionCheckV2, queries.Queries, AuthNKeyColumnResourceOwner, AuthNKeyColumnIdentifier) + } eq := sq.Eq{ AuthNKeyColumnEnabled.identifier(): true, AuthNKeyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -249,6 +294,22 @@ func NewAuthNKeyObjectIDQuery(id string) (SearchQuery, error) { return NewTextQuery(AuthNKeyColumnObjectID, id, TextEquals) } +func NewAuthNKeyIDQuery(id string) (SearchQuery, error) { + return NewTextQuery(AuthNKeyColumnID, id, TextEquals) +} + +func NewAuthNKeyIdentifyerQuery(id string) (SearchQuery, error) { + return NewTextQuery(AuthNKeyColumnIdentifier, id, TextEquals) +} + +func NewAuthNKeyCreationDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(AuthNKeyColumnCreationDate, ts, compare) +} + +func NewAuthNKeyExpirationDateDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(AuthNKeyColumnExpiration, ts, compare) +} + //go:embed authn_key_user.sql var authNKeyUserQuery string @@ -288,49 +349,52 @@ func (q *Queries) GetAuthNKeyUser(ctx context.Context, keyID, userID string) (_ } func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys, error)) { - return sq.Select( - AuthNKeyColumnID.identifier(), - AuthNKeyColumnCreationDate.identifier(), - AuthNKeyColumnChangeDate.identifier(), - AuthNKeyColumnResourceOwner.identifier(), - AuthNKeyColumnSequence.identifier(), - AuthNKeyColumnExpiration.identifier(), - AuthNKeyColumnType.identifier(), - countColumn.identifier(), - ).From(authNKeyTable.identifier()). - PlaceholderFormat(sq.Dollar), - func(rows *sql.Rows) (*AuthNKeys, error) { - authNKeys := make([]*AuthNKey, 0) - var count uint64 - for rows.Next() { - authNKey := new(AuthNKey) - err := rows.Scan( - &authNKey.ID, - &authNKey.CreationDate, - &authNKey.ChangeDate, - &authNKey.ResourceOwner, - &authNKey.Sequence, - &authNKey.Expiration, - &authNKey.Type, - &count, - ) - if err != nil { - return nil, err - } - authNKeys = append(authNKeys, authNKey) - } + query := sq.Select( + AuthNKeyColumnID.identifier(), + AuthNKeyColumnAggregateID.identifier(), + AuthNKeyColumnCreationDate.identifier(), + AuthNKeyColumnChangeDate.identifier(), + AuthNKeyColumnResourceOwner.identifier(), + AuthNKeyColumnSequence.identifier(), + AuthNKeyColumnExpiration.identifier(), + AuthNKeyColumnType.identifier(), + countColumn.identifier(), + ).From(authNKeyTable.identifier()). + PlaceholderFormat(sq.Dollar) - if err := rows.Close(); err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgfn3", "Errors.Query.CloseRows") + return query, func(rows *sql.Rows) (*AuthNKeys, error) { + authNKeys := make([]*AuthNKey, 0) + var count uint64 + for rows.Next() { + authNKey := new(AuthNKey) + err := rows.Scan( + &authNKey.ID, + &authNKey.AggregateID, + &authNKey.CreationDate, + &authNKey.ChangeDate, + &authNKey.ResourceOwner, + &authNKey.Sequence, + &authNKey.Expiration, + &authNKey.Type, + &count, + ) + if err != nil { + return nil, err } - - return &AuthNKeys{ - AuthNKeys: authNKeys, - SearchResponse: SearchResponse{ - Count: count, - }, - }, nil + authNKeys = append(authNKeys, authNKey) } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-Dgfn3", "Errors.Query.CloseRows") + } + + return &AuthNKeys{ + AuthNKeys: authNKeys, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } } func prepareAuthNKeyQuery() (sq.SelectBuilder, func(row *sql.Row) (*AuthNKey, error)) { diff --git a/internal/query/authn_key_test.go b/internal/query/authn_key_test.go index c7441f8dae..b7c66cc665 100644 --- a/internal/query/authn_key_test.go +++ b/internal/query/authn_key_test.go @@ -19,6 +19,7 @@ import ( var ( prepareAuthNKeysStmt = `SELECT projections.authn_keys2.id,` + + ` projections.authn_keys2.aggregate_id,` + ` projections.authn_keys2.creation_date,` + ` projections.authn_keys2.change_date,` + ` projections.authn_keys2.resource_owner,` + @@ -29,6 +30,7 @@ var ( ` FROM projections.authn_keys2` prepareAuthNKeysCols = []string{ "id", + "aggregate_id", "creation_date", "change_date", "resource_owner", @@ -120,6 +122,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { [][]driver.Value{ { "id", + "aggId", testNow, testNow, "ro", @@ -137,6 +140,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { AuthNKeys: []*AuthNKey{ { ID: "id", + AggregateID: "aggId", CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", @@ -157,6 +161,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { [][]driver.Value{ { "id-1", + "aggId-1", testNow, testNow, "ro", @@ -166,6 +171,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { }, { "id-2", + "aggId-2", testNow, testNow, "ro", @@ -183,6 +189,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { AuthNKeys: []*AuthNKey{ { ID: "id-1", + AggregateID: "aggId-1", CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", @@ -192,6 +199,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { }, { ID: "id-2", + AggregateID: "aggId-2", CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", diff --git a/internal/query/current_state.go b/internal/query/current_state.go index 6fae52713f..d0a5b369bf 100644 --- a/internal/query/current_state.go +++ b/internal/query/current_state.go @@ -10,6 +10,7 @@ import ( "time" sq "github.com/Masterminds/squirrel" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/eventstore" @@ -25,7 +26,7 @@ type Stateful interface { type State struct { LastRun time.Time - Position float64 + Position decimal.Decimal EventCreatedAt time.Time AggregateID string AggregateType eventstore.AggregateType @@ -220,7 +221,7 @@ func prepareLatestState() (sq.SelectBuilder, func(*sql.Row) (*State, error)) { var ( creationDate sql.NullTime lastUpdated sql.NullTime - position sql.NullFloat64 + position decimal.NullDecimal ) err := row.Scan( &creationDate, @@ -233,7 +234,7 @@ func prepareLatestState() (sq.SelectBuilder, func(*sql.Row) (*State, error)) { return &State{ EventCreatedAt: creationDate.Time, LastRun: lastUpdated.Time, - Position: position.Float64, + Position: position.Decimal, }, nil } } @@ -258,7 +259,7 @@ func prepareCurrentStateQuery() (sq.SelectBuilder, func(*sql.Rows) (*CurrentStat var ( lastRun sql.NullTime eventDate sql.NullTime - currentPosition sql.NullFloat64 + currentPosition decimal.NullDecimal aggregateType sql.NullString aggregateID sql.NullString sequence sql.NullInt64 @@ -279,7 +280,7 @@ func prepareCurrentStateQuery() (sq.SelectBuilder, func(*sql.Rows) (*CurrentStat } currentState.State.EventCreatedAt = eventDate.Time currentState.State.LastRun = lastRun.Time - currentState.Position = currentPosition.Float64 + currentState.Position = currentPosition.Decimal currentState.AggregateType = eventstore.AggregateType(aggregateType.String) currentState.AggregateID = aggregateID.String currentState.Sequence = uint64(sequence.Int64) diff --git a/internal/query/current_state_test.go b/internal/query/current_state_test.go index c0895dc439..29761b8cb3 100644 --- a/internal/query/current_state_test.go +++ b/internal/query/current_state_test.go @@ -7,6 +7,8 @@ import ( "fmt" "regexp" "testing" + + "github.com/shopspring/decimal" ) var ( @@ -86,7 +88,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { State: State{ EventCreatedAt: testNow, LastRun: testNow, - Position: 20211108, + Position: decimal.NewFromInt(20211108), AggregateID: "agg-id", AggregateType: "agg-type", Sequence: 20211108, @@ -133,7 +135,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { ProjectionName: "projection-name", State: State{ EventCreatedAt: testNow, - Position: 20211108, + Position: decimal.NewFromInt(20211108), LastRun: testNow, AggregateID: "agg-id", AggregateType: "agg-type", @@ -144,7 +146,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { ProjectionName: "projection-name2", State: State{ EventCreatedAt: testNow, - Position: 20211108, + Position: decimal.NewFromInt(20211108), LastRun: testNow, AggregateID: "agg-id", AggregateType: "agg-type", diff --git a/internal/query/execution.go b/internal/query/execution.go index 0a2a989918..4739a5839e 100644 --- a/internal/query/execution.go +++ b/internal/query/execution.go @@ -1,11 +1,13 @@ package query import ( + "cmp" "context" "database/sql" _ "embed" "encoding/json" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -301,13 +303,15 @@ func executionTargetsUnmarshal(data []byte) ([]*exec.Target, error) { } targets := make([]*exec.Target, len(executionTargets)) - // position starts with 1 - for _, item := range executionTargets { + slices.SortFunc(executionTargets, func(a, b *executionTarget) int { + return cmp.Compare(a.Position, b.Position) + }) + for i, item := range executionTargets { if item.Target != "" { - targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: item.Target} + targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: item.Target} } if item.Include != "" { - targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: item.Include} + targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: item.Include} } } return targets, nil diff --git a/internal/query/execution_targets.sql b/internal/query/execution_targets.sql index 32257f4a1f..a6e6dd6caa 100644 --- a/internal/query/execution_targets.sql +++ b/internal/query/execution_targets.sql @@ -1,11 +1,15 @@ -SELECT instance_id, - execution_id, +SELECT et.instance_id, + et.execution_id, JSONB_AGG( JSON_OBJECT( - 'position' : position, - 'include' : include, - 'target' : target_id - ) - ) as targets -FROM projections.executions1_targets -GROUP BY instance_id, execution_id \ No newline at end of file + 'position' : et.position, + 'include' : et.include, + 'target' : et.target_id + ) + ) as targets +FROM projections.executions1_targets AS et + INNER JOIN projections.targets2 AS t + ON et.instance_id = t.instance_id + AND et.target_id IS NOT NULL + AND et.target_id = t.id +GROUP BY et.instance_id, et.execution_id \ No newline at end of file diff --git a/internal/query/execution_test.go b/internal/query/execution_test.go index eaaac1e9ba..64f9a4849f 100644 --- a/internal/query/execution_test.go +++ b/internal/query/execution_test.go @@ -22,9 +22,10 @@ var ( ` COUNT(*) OVER ()` + ` FROM projections.executions1` + ` JOIN (` + - `SELECT instance_id, execution_id, JSONB_AGG( JSON_OBJECT( 'position' : position, 'include' : include, 'target' : target_id ) ) as targets` + - ` FROM projections.executions1_targets` + - ` GROUP BY instance_id, execution_id` + + `SELECT et.instance_id, et.execution_id, JSONB_AGG( JSON_OBJECT( 'position' : et.position, 'include' : et.include, 'target' : et.target_id ) ) as targets` + + ` FROM projections.executions1_targets AS et` + + ` INNER JOIN projections.targets2 AS t ON et.instance_id = t.instance_id AND et.target_id IS NOT NULL AND et.target_id = t.id` + + ` GROUP BY et.instance_id, et.execution_id` + `)` + ` AS execution_targets` + ` ON execution_targets.instance_id = projections.executions1.instance_id` + @@ -45,9 +46,10 @@ var ( ` execution_targets.targets` + ` FROM projections.executions1` + ` JOIN (` + - `SELECT instance_id, execution_id, JSONB_AGG( JSON_OBJECT( 'position' : position, 'include' : include, 'target' : target_id ) ) as targets` + - ` FROM projections.executions1_targets` + - ` GROUP BY instance_id, execution_id` + + `SELECT et.instance_id, et.execution_id, JSONB_AGG( JSON_OBJECT( 'position' : et.position, 'include' : et.include, 'target' : et.target_id ) ) as targets` + + ` FROM projections.executions1_targets AS et` + + ` INNER JOIN projections.targets2 AS t ON et.instance_id = t.instance_id AND et.target_id IS NOT NULL AND et.target_id = t.id` + + ` GROUP BY et.instance_id, et.execution_id` + `)` + ` AS execution_targets` + ` ON execution_targets.instance_id = projections.executions1.instance_id` + @@ -179,6 +181,63 @@ func Test_ExecutionPrepares(t *testing.T) { }, }, }, + { + name: "prepareExecutionsQuery multiple result, removed target, position missing", + prepare: prepareExecutionsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareExecutionsStmt), + prepareExecutionsCols, + [][]driver.Value{ + { + "ro", + "id-1", + testNow, + testNow, + []byte(`[{"position" : 1, "target" : "target"}, {"position" : 3, "include" : "include"}]`), + }, + { + "ro", + "id-2", + testNow, + testNow, + []byte(`[{"position" : 2, "target" : "target"}, {"position" : 1, "include" : "include"}]`), + }, + }, + ), + }, + object: &Executions{ + SearchResponse: SearchResponse{ + Count: 2, + }, + Executions: []*Execution{ + { + ObjectDetails: domain.ObjectDetails{ + ID: "id-1", + EventDate: testNow, + CreationDate: testNow, + ResourceOwner: "ro", + }, + Targets: []*exec.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, + }, + { + ObjectDetails: domain.ObjectDetails{ + ID: "id-2", + EventDate: testNow, + CreationDate: testNow, + ResourceOwner: "ro", + }, + Targets: []*exec.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + }, + }, + }, + }, { name: "prepareExecutionsQuery sql err", prepare: prepareExecutionsQuery, diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index f51e9a11a7..c1c47e73bc 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -165,6 +165,7 @@ type SAMLIDPTemplate struct { WithSignedRequest bool NameIDFormat sql.Null[domain.SAMLNameIDFormat] TransientMappingAttributeName string + FederatedLogoutEnabled bool } var ( @@ -724,6 +725,10 @@ var ( name: projection.SAMLTransientMappingAttributeName, table: samlIdpTemplateTable, } + SAMLFederatedLogoutEnabledCol = Column{ + name: projection.SAMLFederatedLogoutEnabled, + table: samlIdpTemplateTable, + } ) // IDPTemplateByID searches for the requested id with permission check if necessary @@ -948,6 +953,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla SAMLWithSignedRequestCol.identifier(), SAMLNameIDFormatCol.identifier(), SAMLTransientMappingAttributeNameCol.identifier(), + SAMLFederatedLogoutEnabledCol.identifier(), // ldap LDAPIDCol.identifier(), LDAPServersCol.identifier(), @@ -1067,6 +1073,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla samlWithSignedRequest := sql.NullBool{} samlNameIDFormat := sql.Null[domain.SAMLNameIDFormat]{} samlTransientMappingAttributeName := sql.NullString{} + samlFederatedLogoutEnabled := sql.NullBool{} ldapID := sql.NullString{} ldapServers := database.TextArray[string]{} @@ -1184,6 +1191,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla &samlWithSignedRequest, &samlNameIDFormat, &samlTransientMappingAttributeName, + &samlFederatedLogoutEnabled, // ldap &ldapID, &ldapServers, @@ -1323,6 +1331,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla WithSignedRequest: samlWithSignedRequest.Bool, NameIDFormat: samlNameIDFormat, TransientMappingAttributeName: samlTransientMappingAttributeName.String, + FederatedLogoutEnabled: samlFederatedLogoutEnabled.Bool, } } if ldapID.Valid { @@ -1456,6 +1465,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate SAMLWithSignedRequestCol.identifier(), SAMLNameIDFormatCol.identifier(), SAMLTransientMappingAttributeNameCol.identifier(), + SAMLFederatedLogoutEnabledCol.identifier(), // ldap LDAPIDCol.identifier(), LDAPServersCol.identifier(), @@ -1580,6 +1590,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate samlWithSignedRequest := sql.NullBool{} samlNameIDFormat := sql.Null[domain.SAMLNameIDFormat]{} samlTransientMappingAttributeName := sql.NullString{} + samlFederatedLogoutEnabled := sql.NullBool{} ldapID := sql.NullString{} ldapServers := database.TextArray[string]{} @@ -1697,6 +1708,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate &samlWithSignedRequest, &samlNameIDFormat, &samlTransientMappingAttributeName, + &samlFederatedLogoutEnabled, // ldap &ldapID, &ldapServers, @@ -1835,6 +1847,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate WithSignedRequest: samlWithSignedRequest.Bool, NameIDFormat: samlNameIDFormat, TransientMappingAttributeName: samlTransientMappingAttributeName.String, + FederatedLogoutEnabled: samlFederatedLogoutEnabled.Bool, } } if ldapID.Valid { diff --git a/internal/query/idp_template_test.go b/internal/query/idp_template_test.go index 702e5d0ced..aed243c61e 100644 --- a/internal/query/idp_template_test.go +++ b/internal/query/idp_template_test.go @@ -99,6 +99,7 @@ var ( ` projections.idp_templates6_saml.with_signed_request,` + ` projections.idp_templates6_saml.name_id_format,` + ` projections.idp_templates6_saml.transient_mapping_attribute_name,` + + ` projections.idp_templates6_saml.federated_logout_enabled,` + // ldap ` projections.idp_templates6_ldap2.idp_id,` + ` projections.idp_templates6_ldap2.servers,` + @@ -228,6 +229,7 @@ var ( "with_signed_request", "name_id_format", "transient_mapping_attribute_name", + "federated_logout_enabled", // ldap config "idp_id", "servers", @@ -344,6 +346,7 @@ var ( ` projections.idp_templates6_saml.with_signed_request,` + ` projections.idp_templates6_saml.name_id_format,` + ` projections.idp_templates6_saml.transient_mapping_attribute_name,` + + ` projections.idp_templates6_saml.federated_logout_enabled,` + // ldap ` projections.idp_templates6_ldap2.idp_id,` + ` projections.idp_templates6_ldap2.servers,` + @@ -474,6 +477,7 @@ var ( "with_signed_request", "name_id_format", "transient_mapping_attribute_name", + "federated_logout_enabled", // ldap config "idp_id", "servers", @@ -630,6 +634,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -784,6 +789,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -936,6 +942,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -1086,6 +1093,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -1235,6 +1243,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -1384,6 +1393,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -1534,6 +1544,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -1683,6 +1694,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { false, domain.SAMLNameIDFormatTransient, "customAttribute", + true, // ldap config nil, nil, @@ -1742,6 +1754,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { WithSignedRequest: false, NameIDFormat: sql.Null[domain.SAMLNameIDFormat]{V: domain.SAMLNameIDFormatTransient, Valid: true}, TransientMappingAttributeName: "customAttribute", + FederatedLogoutEnabled: true, }, }, }, @@ -1836,6 +1849,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config "idp-id", database.TextArray[string]{"server"}, @@ -2006,6 +2020,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -2157,6 +2172,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -2336,6 +2352,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config "idp-id", database.TextArray[string]{"server"}, @@ -2515,6 +2532,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -2667,6 +2685,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config "idp-id-ldap", database.TextArray[string]{"server"}, @@ -2784,6 +2803,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { false, domain.SAMLNameIDFormatTransient, "customAttribute", + true, // ldap config nil, nil, @@ -2901,6 +2921,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -3018,6 +3039,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -3135,6 +3157,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -3252,6 +3275,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -3360,6 +3384,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { WithSignedRequest: false, NameIDFormat: sql.Null[domain.SAMLNameIDFormat]{V: domain.SAMLNameIDFormatTransient, Valid: true}, TransientMappingAttributeName: "customAttribute", + FederatedLogoutEnabled: true, }, }, { diff --git a/internal/query/instance.go b/internal/query/instance.go index 1b3bb055cb..bb311cbb85 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -150,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() + filter, query, scan := prepareInstancesQuery(queries.SortingColumn, queries.Asc) stmt, args, err := query(queries.toQuery(filter)).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-M9fow", "Errors.Query.SQLStatement") @@ -260,17 +260,20 @@ func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag { return instance.DefaultLang } -func prepareInstancesQuery() (sq.SelectBuilder, func(sq.SelectBuilder) sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { +func prepareInstancesQuery(sortBy Column, isAscedingSort bool) (sq.SelectBuilder, func(sq.SelectBuilder) sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { instanceFilterTable := instanceTable.setAlias(InstancesFilterTableAlias) instanceFilterIDColumn := InstanceColumnID.setTable(instanceFilterTable) instanceFilterCountColumn := InstancesFilterTableAlias + ".count" - return sq.Select( - InstanceColumnID.identifier(), - countColumn.identifier(), - ).Distinct().From(instanceTable.identifier()). + + selector := sq.Select(InstanceColumnID.identifier(), countColumn.identifier()) + if !sortBy.isZero() { + selector = sq.Select(InstanceColumnID.identifier(), countColumn.identifier(), sortBy.identifier()) + } + + return selector.Distinct().From(instanceTable.identifier()). LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)), func(builder sq.SelectBuilder) sq.SelectBuilder { - return sq.Select( + outerQuery := sq.Select( instanceFilterCountColumn, instanceFilterIDColumn.identifier(), InstanceColumnCreationDate.identifier(), @@ -292,6 +295,16 @@ func prepareInstancesQuery() (sq.SelectBuilder, func(sq.SelectBuilder) sq.Select LeftJoin(join(InstanceColumnID, instanceFilterIDColumn)). LeftJoin(join(InstanceDomainInstanceIDCol, instanceFilterIDColumn)). PlaceholderFormat(sq.Dollar) + + if !sortBy.isZero() { + sorting := sortBy.identifier() + if !isAscedingSort { + sorting += " DESC" + } + return outerQuery.OrderBy(sorting) + } + + return outerQuery }, func(rows *sql.Rows) (*Instances, error) { instances := make([]*Instance, 0) diff --git a/internal/query/instance_test.go b/internal/query/instance_test.go index 55b1c8314b..37adfc8605 100644 --- a/internal/query/instance_test.go +++ b/internal/query/instance_test.go @@ -70,7 +70,7 @@ func Test_InstancePrepares(t *testing.T) { { name: "prepareInstancesQuery no result", prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(Column{}, true) return query(filter), scan }, want: want{ @@ -85,7 +85,7 @@ func Test_InstancePrepares(t *testing.T) { { name: "prepareInstancesQuery one result", prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(Column{}, true) return query(filter), scan }, want: want{ @@ -149,7 +149,7 @@ func Test_InstancePrepares(t *testing.T) { { name: "prepareInstancesQuery multiple results", prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(Column{}, true) return query(filter), scan }, want: want{ @@ -253,7 +253,8 @@ func Test_InstancePrepares(t *testing.T) { IsPrimary: true, }, }, - }, { + }, + { ID: "id2", CreationDate: testNow, ChangeDate: testNow, @@ -282,7 +283,7 @@ func Test_InstancePrepares(t *testing.T) { { name: "prepareInstancesQuery sql err", prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(Column{}, true) return query(filter), scan }, want: want{ diff --git a/internal/query/oidc_settings.go b/internal/query/oidc_settings.go index bdd21cfd15..4ecd6cdad2 100644 --- a/internal/query/oidc_settings.go +++ b/internal/query/oidc_settings.go @@ -84,7 +84,7 @@ func (q *Queries) OIDCSettingsByAggID(ctx context.Context, aggregateID string) ( OIDCSettingsColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-s9nle", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-s9nle", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/org.go b/internal/query/org.go index dfe90ad9f8..e2d9e205da 100644 --- a/internal/query/org.go +++ b/internal/query/org.go @@ -12,6 +12,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" domain_pkg "github.com/zitadel/zitadel/internal/domain" + es "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/query/projection" @@ -77,6 +78,8 @@ type Org struct { ResourceOwner string State domain_pkg.OrgState Sequence uint64 + // instanceID is used to create a unique cache key for the org + instanceID string Name string Domain string @@ -122,7 +125,7 @@ func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if org, ok := q.caches.org.Get(ctx, orgIndexByID, id); ok { + if org, ok := q.caches.org.Get(ctx, orgIndexByID, orgCacheKey(authz.GetInstance(ctx).InstanceID(), id)); ok { return org, nil } defer func() { @@ -159,6 +162,7 @@ func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string ResourceOwner: foundOrg.Owner, State: domain_pkg.OrgState(foundOrg.State.State), Sequence: uint64(foundOrg.Sequence), + instanceID: foundOrg.InstanceID, Name: foundOrg.Name, Domain: foundOrg.PrimaryDomain.Domain, }, nil @@ -195,7 +199,7 @@ func (q *Queries) OrgByPrimaryDomain(ctx context.Context, domain string) (org *O ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - org, ok := q.caches.org.Get(ctx, orgIndexByPrimaryDomain, domain) + org, ok := q.caches.org.Get(ctx, orgIndexByPrimaryDomain, orgCacheKey(authz.GetInstance(ctx).InstanceID(), domain)) if ok { return org, nil } @@ -430,6 +434,7 @@ func prepareOrgQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { OrgColumnResourceOwner.identifier(), OrgColumnState.identifier(), OrgColumnSequence.identifier(), + OrgColumnInstanceID.identifier(), OrgColumnName.identifier(), OrgColumnDomain.identifier(), ). @@ -444,6 +449,7 @@ func prepareOrgQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { &o.ResourceOwner, &o.State, &o.Sequence, + &o.instanceID, &o.Name, &o.Domain, ) @@ -521,15 +527,21 @@ const ( func (o *Org) Keys(index orgIndex) []string { switch index { case orgIndexByID: - return []string{o.ID} + return []string{orgCacheKey(o.instanceID, o.ID)} case orgIndexByPrimaryDomain: - return []string{o.Domain} + return []string{orgCacheKey(o.instanceID, o.Domain)} case orgIndexUnspecified: } return nil } +func orgCacheKey(instanceID, key string) string { + return instanceID + "-" + key +} + func (c *Caches) registerOrgInvalidation() { - invalidate := cacheInvalidationFunc(c.org, orgIndexByID, getAggregateID) + invalidate := cacheInvalidationFunc(c.org, orgIndexByID, func(aggregate *es.Aggregate) string { + return orgCacheKey(aggregate.InstanceID, aggregate.ID) + }) projection.OrgProjection.RegisterCacheInvalidation(invalidate) } diff --git a/internal/query/org_metadata.go b/internal/query/org_metadata.go index 84b204de2b..e67c7222cd 100644 --- a/internal/query/org_metadata.go +++ b/internal/query/org_metadata.go @@ -103,7 +103,7 @@ func (q *Queries) GetOrgMetadataByKey(ctx context.Context, shouldTriggerBulk boo } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-aDaG2", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-aDaG2", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -133,7 +133,7 @@ func (q *Queries) SearchOrgMetadata(ctx context.Context, shouldTriggerBulk bool, 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") + return nil, zerrors.ThrowInternal(err, "QUERY-Egbld", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { @@ -194,7 +194,6 @@ func prepareOrgMetadataQuery() (sq.SelectBuilder, func(*sql.Row) (*OrgMetadata, &m.Key, &m.Value, ) - if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, zerrors.ThrowNotFound(err, "QUERY-Rph32", "Errors.Metadata.NotFound") diff --git a/internal/query/org_test.go b/internal/query/org_test.go index d704d2901a..635594e7fd 100644 --- a/internal/query/org_test.go +++ b/internal/query/org_test.go @@ -50,6 +50,7 @@ var ( ` projections.orgs1.resource_owner,` + ` projections.orgs1.org_state,` + ` projections.orgs1.sequence,` + + ` projections.orgs1.instance_id,` + ` projections.orgs1.name,` + ` projections.orgs1.primary_domain` + ` FROM projections.orgs1` @@ -60,6 +61,7 @@ var ( "resource_owner", "org_state", "sequence", + "instance_id", "name", "primary_domain", } @@ -242,6 +244,7 @@ func Test_OrgPrepares(t *testing.T) { "ro", domain.OrgStateActive, uint64(20211108), + "instance-id", "org-name", "zitadel.ch", }, @@ -254,6 +257,7 @@ func Test_OrgPrepares(t *testing.T) { ResourceOwner: "ro", State: domain.OrgStateActive, Sequence: 20211108, + instanceID: "instance-id", Name: "org-name", Domain: "zitadel.ch", }, diff --git a/internal/query/project.go b/internal/query/project.go index 7501047182..59e2dd95c0 100644 --- a/internal/query/project.go +++ b/internal/query/project.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -72,11 +73,94 @@ var ( } ) +var ( + grantedProjectsAlias = table{ + name: "granted_projects", + instanceIDCol: projection.ProjectColumnInstanceID, + } + GrantedProjectColumnID = Column{ + name: projection.ProjectColumnID, + table: grantedProjectsAlias, + } + GrantedProjectColumnCreationDate = Column{ + name: projection.ProjectColumnCreationDate, + table: grantedProjectsAlias, + } + GrantedProjectColumnChangeDate = Column{ + name: projection.ProjectColumnChangeDate, + table: grantedProjectsAlias, + } + grantedProjectColumnResourceOwner = Column{ + name: projection.ProjectColumnResourceOwner, + table: grantedProjectsAlias, + } + grantedProjectColumnInstanceID = Column{ + name: projection.ProjectGrantColumnInstanceID, + table: grantedProjectsAlias, + } + grantedProjectColumnState = Column{ + name: "project_state", + table: grantedProjectsAlias, + } + GrantedProjectColumnName = Column{ + name: "project_name", + table: grantedProjectsAlias, + } + grantedProjectColumnProjectRoleAssertion = Column{ + name: projection.ProjectColumnProjectRoleAssertion, + table: grantedProjectsAlias, + } + grantedProjectColumnProjectRoleCheck = Column{ + name: projection.ProjectColumnProjectRoleCheck, + table: grantedProjectsAlias, + } + grantedProjectColumnHasProjectCheck = Column{ + name: projection.ProjectColumnHasProjectCheck, + table: grantedProjectsAlias, + } + grantedProjectColumnPrivateLabelingSetting = Column{ + name: projection.ProjectColumnPrivateLabelingSetting, + table: grantedProjectsAlias, + } + grantedProjectColumnGrantResourceOwner = Column{ + name: "project_grant_resource_owner", + table: grantedProjectsAlias, + } + grantedProjectColumnGrantID = Column{ + name: projection.ProjectGrantColumnGrantID, + table: grantedProjectsAlias, + } + grantedProjectColumnGrantedOrganization = Column{ + name: projection.ProjectGrantColumnGrantedOrgID, + table: grantedProjectsAlias, + } + grantedProjectColumnGrantedOrganizationName = Column{ + name: "granted_org_name", + table: grantedProjectsAlias, + } + grantedProjectColumnGrantState = Column{ + name: "project_grant_state", + table: grantedProjectsAlias, + } +) + type Projects struct { SearchResponse Projects []*Project } +func projectsCheckPermission(ctx context.Context, projects *Projects, permissionCheck domain.PermissionCheck) { + projects.Projects = slices.DeleteFunc(projects.Projects, + func(project *Project) bool { + return projectCheckPermission(ctx, project.ResourceOwner, project.ID, permissionCheck) != nil + }, + ) +} + +func projectCheckPermission(ctx context.Context, resourceOwner string, projectID string, permissionCheck domain.PermissionCheck) error { + return permissionCheck(ctx, domain.PermissionProjectRead, resourceOwner, projectID) +} + type Project struct { ID string CreationDate time.Time @@ -94,7 +178,19 @@ type Project struct { type ProjectSearchQueries struct { SearchRequest - Queries []SearchQuery + Queries []SearchQuery + GrantQueries []SearchQuery +} + +func (q *Queries) GetProjectByIDWithPermission(ctx context.Context, shouldTriggerBulk bool, id string, permissionCheck domain.PermissionCheck) (*Project, error) { + project, err := q.ProjectByID(ctx, shouldTriggerBulk, id) + if err != nil { + return nil, err + } + if err := projectCheckPermission(ctx, project.ResourceOwner, project.ID, permissionCheck); err != nil { + return nil, err + } + return project, nil } func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id string) (project *Project, err error) { @@ -115,7 +211,7 @@ func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id st } query, args, err := stmt.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-2m00Q", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-2m00Q", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -125,7 +221,18 @@ func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id st return project, err } -func (q *Queries) SearchProjects(ctx context.Context, queries *ProjectSearchQueries) (projects *Projects, err error) { +func (q *Queries) SearchProjects(ctx context.Context, queries *ProjectSearchQueries, permissionCheck domain.PermissionCheck) (*Projects, error) { + projects, err := q.searchProjects(ctx, queries) + if err != nil { + return nil, err + } + if permissionCheck != nil { + projectsCheckPermission(ctx, projects, permissionCheck) + } + return projects, nil +} + +func (q *Queries) searchProjects(ctx context.Context, queries *ProjectSearchQueries) (projects *Projects, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -147,6 +254,103 @@ func (q *Queries) SearchProjects(ctx context.Context, queries *ProjectSearchQuer return projects, err } +type ProjectAndGrantedProjectSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + +func (q *ProjectAndGrantedProjectSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func projectPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectAndGrantedProjectSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + grantedProjectColumnResourceOwner, + domain.PermissionProjectRead, + SingleOrgPermissionOption(queries.Queries), + WithProjectsPermissionOption(GrantedProjectColumnID), + ) + return query.JoinClause(join, args...) +} + +func (q *Queries) SearchGrantedProjects(ctx context.Context, queries *ProjectAndGrantedProjectSearchQueries, permissionCheck domain.PermissionCheck) (*GrantedProjects, error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + projects, err := q.searchGrantedProjects(ctx, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + grantedProjectsCheckPermission(ctx, projects, permissionCheck) + } + return projects, nil +} + +func (q *Queries) searchGrantedProjects(ctx context.Context, queries *ProjectAndGrantedProjectSearchQueries, permissionCheckV2 bool) (grantedProjects *GrantedProjects, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + query, scan := prepareGrantedProjectsQuery() + query = projectPermissionCheckV2(ctx, query, permissionCheckV2, queries) + eq := sq.Eq{grantedProjectColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} + stmt, args, err := queries.toQuery(query).Where(eq).ToSql() + if err != nil { + return nil, zerrors.ThrowInvalidArgument(err, "QUERY-T84X9", "Errors.Query.InvalidRequest") + } + + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + grantedProjects, err = scan(rows) + return err + }, stmt, args...) + if err != nil { + return nil, err + } + return grantedProjects, nil +} + +func NewGrantedProjectNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { + return NewTextQuery(GrantedProjectColumnName, value, method) +} + +func NewGrantedProjectResourceOwnerSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(grantedProjectColumnResourceOwner, value, TextEquals) +} + +func NewGrantedProjectIDSearchQuery(ids []string) (SearchQuery, error) { + list := make([]interface{}, len(ids)) + for i, value := range ids { + list[i] = value + } + return NewListQuery(GrantedProjectColumnID, list, ListIn) +} + +func NewGrantedProjectOrganizationIDSearchQuery(value string) (SearchQuery, error) { + project, err := NewGrantedProjectResourceOwnerSearchQuery(value) + if err != nil { + return nil, err + } + grant, err := NewGrantedProjectGrantedOrganizationIDSearchQuery(value) + if err != nil { + return nil, err + } + return NewOrQuery(project, grant) +} + +func NewGrantedProjectGrantResourceOwnerSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(grantedProjectColumnGrantResourceOwner, value, TextEquals) +} + +func NewGrantedProjectGrantedOrganizationIDSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(grantedProjectColumnGrantedOrganization, value, TextEquals) +} + func NewProjectNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { return NewTextQuery(ProjectColumnName, value, method) } @@ -285,3 +489,183 @@ func prepareProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Projects, error }, nil } } + +type GrantedProjects struct { + SearchResponse + GrantedProjects []*GrantedProject +} + +func grantedProjectsCheckPermission(ctx context.Context, grantedProjects *GrantedProjects, permissionCheck domain.PermissionCheck) { + grantedProjects.GrantedProjects = slices.DeleteFunc(grantedProjects.GrantedProjects, + func(grantedProject *GrantedProject) bool { + if grantedProject.GrantedOrgID != "" { + return projectGrantCheckPermission(ctx, grantedProject.ResourceOwner, grantedProject.ProjectID, grantedProject.GrantID, grantedProject.GrantedOrgID, permissionCheck) != nil + } + return projectCheckPermission(ctx, grantedProject.ResourceOwner, grantedProject.ProjectID, permissionCheck) != nil + }, + ) +} + +type GrantedProject struct { + ProjectID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + InstanceID string + ProjectState domain.ProjectState + ProjectName string + + ProjectRoleAssertion bool + ProjectRoleCheck bool + HasProjectCheck bool + PrivateLabelingSetting domain.PrivateLabelingSetting + + GrantID string + GrantedOrgID string + OrgName string + ProjectGrantState domain.ProjectGrantState +} + +func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedProjects, error)) { + return sq.Select( + GrantedProjectColumnID.identifier(), + GrantedProjectColumnCreationDate.identifier(), + GrantedProjectColumnChangeDate.identifier(), + grantedProjectColumnResourceOwner.identifier(), + grantedProjectColumnInstanceID.identifier(), + grantedProjectColumnState.identifier(), + GrantedProjectColumnName.identifier(), + grantedProjectColumnProjectRoleAssertion.identifier(), + grantedProjectColumnProjectRoleCheck.identifier(), + grantedProjectColumnHasProjectCheck.identifier(), + grantedProjectColumnPrivateLabelingSetting.identifier(), + grantedProjectColumnGrantID.identifier(), + grantedProjectColumnGrantedOrganization.identifier(), + grantedProjectColumnGrantedOrganizationName.identifier(), + grantedProjectColumnGrantState.identifier(), + countColumn.identifier(), + ).From(getProjectsAndGrantedProjectsFromQuery()). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*GrantedProjects, error) { + projects := make([]*GrantedProject, 0) + var ( + count uint64 + grantID = sql.NullString{} + orgID = sql.NullString{} + orgName = sql.NullString{} + projectGrantState = sql.NullInt16{} + ) + for rows.Next() { + grantedProject := new(GrantedProject) + err := rows.Scan( + &grantedProject.ProjectID, + &grantedProject.CreationDate, + &grantedProject.ChangeDate, + &grantedProject.ResourceOwner, + &grantedProject.InstanceID, + &grantedProject.ProjectState, + &grantedProject.ProjectName, + &grantedProject.ProjectRoleAssertion, + &grantedProject.ProjectRoleCheck, + &grantedProject.HasProjectCheck, + &grantedProject.PrivateLabelingSetting, + &grantID, + &orgID, + &orgName, + &projectGrantState, + &count, + ) + if err != nil { + return nil, err + } + if grantID.Valid { + grantedProject.GrantID = grantID.String + } + if orgID.Valid { + grantedProject.GrantedOrgID = orgID.String + } + if orgName.Valid { + grantedProject.OrgName = orgName.String + } + if projectGrantState.Valid { + grantedProject.ProjectGrantState = domain.ProjectGrantState(projectGrantState.Int16) + } + projects = append(projects, grantedProject) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-K9gEE", "Errors.Query.CloseRows") + } + + return &GrantedProjects{ + GrantedProjects: projects, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} + +func getProjectsAndGrantedProjectsFromQuery() string { + return "(" + + prepareProjects() + + " UNION ALL " + + prepareGrantedProjects() + + ") AS " + grantedProjectsAlias.identifier() +} + +func prepareProjects() string { + builder := sq.Select( + ProjectColumnID.identifier()+" AS "+GrantedProjectColumnID.name, + ProjectColumnCreationDate.identifier()+" AS "+GrantedProjectColumnCreationDate.name, + ProjectColumnChangeDate.identifier()+" AS "+GrantedProjectColumnChangeDate.name, + ProjectColumnResourceOwner.identifier()+" AS "+grantedProjectColumnResourceOwner.name, + ProjectColumnInstanceID.identifier()+" AS "+grantedProjectColumnInstanceID.name, + ProjectColumnState.identifier()+" AS "+grantedProjectColumnState.name, + ProjectColumnName.identifier()+" AS "+GrantedProjectColumnName.name, + ProjectColumnProjectRoleAssertion.identifier()+" AS "+grantedProjectColumnProjectRoleAssertion.name, + ProjectColumnProjectRoleCheck.identifier()+" AS "+grantedProjectColumnProjectRoleCheck.name, + ProjectColumnHasProjectCheck.identifier()+" AS "+grantedProjectColumnHasProjectCheck.name, + ProjectColumnPrivateLabelingSetting.identifier()+" AS "+grantedProjectColumnPrivateLabelingSetting.name, + "NULL::TEXT AS "+grantedProjectColumnGrantResourceOwner.name, + "NULL::TEXT AS "+grantedProjectColumnGrantID.name, + "NULL::TEXT AS "+grantedProjectColumnGrantedOrganization.name, + "NULL::TEXT AS "+grantedProjectColumnGrantedOrganizationName.name, + "NULL::SMALLINT AS "+grantedProjectColumnGrantState.name, + countColumn.identifier()). + From(projectsTable.identifier()). + PlaceholderFormat(sq.Dollar) + + stmt, _ := builder.MustSql() + return stmt +} + +func prepareGrantedProjects() string { + grantedOrgTable := orgsTable.setAlias(ProjectGrantGrantedOrgTableAlias) + grantedOrgIDColumn := OrgColumnID.setTable(grantedOrgTable) + builder := sq.Select( + ProjectGrantColumnProjectID.identifier()+" AS "+GrantedProjectColumnID.name, + ProjectGrantColumnCreationDate.identifier()+" AS "+GrantedProjectColumnCreationDate.name, + ProjectGrantColumnChangeDate.identifier()+" AS "+GrantedProjectColumnChangeDate.name, + ProjectColumnResourceOwner.identifier()+" AS "+grantedProjectColumnResourceOwner.name, + ProjectGrantColumnInstanceID.identifier()+" AS "+grantedProjectColumnInstanceID.name, + ProjectColumnState.identifier()+" AS "+grantedProjectColumnState.name, + ProjectColumnName.identifier()+" AS "+GrantedProjectColumnName.name, + ProjectColumnProjectRoleAssertion.identifier()+" AS "+grantedProjectColumnProjectRoleAssertion.name, + ProjectColumnProjectRoleCheck.identifier()+" AS "+grantedProjectColumnProjectRoleCheck.name, + ProjectColumnHasProjectCheck.identifier()+" AS "+grantedProjectColumnHasProjectCheck.name, + ProjectColumnPrivateLabelingSetting.identifier()+" AS "+grantedProjectColumnPrivateLabelingSetting.name, + ProjectGrantColumnResourceOwner.identifier()+" AS "+grantedProjectColumnGrantResourceOwner.name, + ProjectGrantColumnGrantID.identifier()+" AS "+grantedProjectColumnGrantID.name, + ProjectGrantColumnGrantedOrgID.identifier()+" AS "+grantedProjectColumnGrantedOrganization.name, + ProjectGrantColumnGrantedOrgName.identifier()+" AS "+grantedProjectColumnGrantedOrganizationName.name, + ProjectGrantColumnState.identifier()+" AS "+grantedProjectColumnGrantState.name, + countColumn.identifier()). + From(projectGrantsTable.identifier()). + PlaceholderFormat(sq.Dollar). + LeftJoin(join(ProjectColumnID, ProjectGrantColumnProjectID)). + LeftJoin(join(grantedOrgIDColumn, ProjectGrantColumnGrantedOrgID)) + + stmt, _ := builder.MustSql() + return stmt +} diff --git a/internal/query/project_grant.go b/internal/query/project_grant.go index b971593c77..1931cad0f5 100644 --- a/internal/query/project_grant.go +++ b/internal/query/project_grant.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -104,6 +105,50 @@ type ProjectGrantSearchQueries struct { Queries []SearchQuery } +func projectGrantsCheckPermission(ctx context.Context, projectGrants *ProjectGrants, permissionCheck domain.PermissionCheck) { + projectGrants.ProjectGrants = slices.DeleteFunc(projectGrants.ProjectGrants, + func(projectGrant *ProjectGrant) bool { + return projectGrantCheckPermission(ctx, projectGrant.ResourceOwner, projectGrant.ProjectID, projectGrant.GrantID, projectGrant.GrantedOrgID, permissionCheck) != nil + }, + ) +} + +func projectGrantCheckPermission(ctx context.Context, resourceOwner, projectID, grantID, grantedOrgID string, permissionCheck domain.PermissionCheck) error { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, grantedOrgID, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, projectID); err != nil { + return err + } + } + } + return nil +} + +// TODO: add permission check on project grant level +func projectGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectGrantSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + ProjectGrantColumnResourceOwner, + domain.PermissionProjectGrantRead, + SingleOrgPermissionOption(queries.Queries), + ) + return query.JoinClause(join, args...) +} + +func (q *Queries) GetProjectGrantByIDWithPermission(ctx context.Context, shouldTriggerBulk bool, id string, permissionCheck domain.PermissionCheck) (*ProjectGrant, error) { + projectGrant, err := q.ProjectGrantByID(ctx, shouldTriggerBulk, id) + if err != nil { + return nil, err + } + if err := projectCheckPermission(ctx, projectGrant.ResourceOwner, projectGrant.GrantID, permissionCheck); err != nil { + return nil, err + } + return projectGrant, nil +} + func (q *Queries) ProjectGrantByID(ctx context.Context, shouldTriggerBulk bool, id string) (grant *ProjectGrant, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -122,7 +167,7 @@ func (q *Queries) ProjectGrantByID(ctx context.Context, shouldTriggerBulk bool, } query, args, err := stmt.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Nf93d", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Nf93d", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -144,7 +189,7 @@ func (q *Queries) ProjectGrantByIDAndGrantedOrg(ctx context.Context, id, granted } query, args, err := stmt.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-MO9fs", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-MO9fs", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -154,11 +199,24 @@ func (q *Queries) ProjectGrantByIDAndGrantedOrg(ctx context.Context, id, granted return grant, err } -func (q *Queries) SearchProjectGrants(ctx context.Context, queries *ProjectGrantSearchQueries) (grants *ProjectGrants, err error) { +func (q *Queries) SearchProjectGrants(ctx context.Context, queries *ProjectGrantSearchQueries, permissionCheck domain.PermissionCheck) (grants *ProjectGrants, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + projectsGrants, err := q.searchProjectGrants(ctx, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + projectGrantsCheckPermission(ctx, projectsGrants, permissionCheck) + } + return projectsGrants, nil +} + +func (q *Queries) searchProjectGrants(ctx context.Context, queries *ProjectGrantSearchQueries, permissionCheckV2 bool) (grants *ProjectGrants, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareProjectGrantsQuery() + query = projectGrantPermissionCheckV2(ctx, query, permissionCheckV2, queries) eq := sq.Eq{ ProjectGrantColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -179,6 +237,7 @@ func (q *Queries) SearchProjectGrants(ctx context.Context, queries *ProjectGrant return grants, err } +// SearchProjectGrantsByProjectIDAndRoleKey is used internally to remove the roles of a project grant, so no permission check necessary func (q *Queries) SearchProjectGrantsByProjectIDAndRoleKey(ctx context.Context, projectID, roleKey string) (projects *ProjectGrants, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -195,13 +254,33 @@ func (q *Queries) SearchProjectGrantsByProjectIDAndRoleKey(ctx context.Context, if err != nil { return nil, err } - return q.SearchProjectGrants(ctx, searchQuery) + return q.SearchProjectGrants(ctx, searchQuery, nil) } func NewProjectGrantProjectIDSearchQuery(value string) (SearchQuery, error) { return NewTextQuery(ProjectGrantColumnProjectID, value, TextEquals) } +func (q *ProjectGrantSearchQueries) AppendPermissionQueries(permissions []string) error { + if !authz.HasGlobalPermission(permissions) { + ids := authz.GetAllPermissionCtxIDs(permissions) + query, err := NewProjectGrantIDsSearchQuery(ids) + if err != nil { + return err + } + q.Queries = append(q.Queries, query) + } + return nil +} + +func NewProjectGrantProjectIDsSearchQuery(ids []string) (SearchQuery, error) { + list := make([]interface{}, len(ids)) + for i, value := range ids { + list[i] = value + } + return NewListQuery(ProjectGrantColumnProjectID, list, ListIn) +} + func NewProjectGrantIDsSearchQuery(values []string) (SearchQuery, error) { list := make([]interface{}, len(values)) for i, value := range values { @@ -243,18 +322,6 @@ func (q *ProjectGrantSearchQueries) AppendGrantedOrgQuery(orgID string) error { return nil } -func (q *ProjectGrantSearchQueries) AppendPermissionQueries(permissions []string) error { - if !authz.HasGlobalPermission(permissions) { - ids := authz.GetAllPermissionCtxIDs(permissions) - query, err := NewProjectGrantIDsSearchQuery(ids) - if err != nil { - return err - } - q.Queries = append(q.Queries, query) - } - return nil -} - func (q *ProjectGrantSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { query = q.SearchRequest.toQuery(query) for _, q := range q.Queries { diff --git a/internal/query/project_role.go b/internal/query/project_role.go index ab4f40ca38..e70fcf277e 100644 --- a/internal/query/project_role.go +++ b/internal/query/project_role.go @@ -3,12 +3,14 @@ package query import ( "context" "database/sql" + "slices" "time" sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + "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" @@ -80,7 +82,45 @@ type ProjectRoleSearchQueries struct { Queries []SearchQuery } -func (q *Queries) SearchProjectRoles(ctx context.Context, shouldTriggerBulk bool, queries *ProjectRoleSearchQueries) (roles *ProjectRoles, err error) { +func projectRolesCheckPermission(ctx context.Context, projectRoles *ProjectRoles, permissionCheck domain.PermissionCheck) { + projectRoles.ProjectRoles = slices.DeleteFunc(projectRoles.ProjectRoles, + func(projectRole *ProjectRole) bool { + return projectRoleCheckPermission(ctx, projectRole.ResourceOwner, projectRole.Key, permissionCheck) != nil + }, + ) +} + +func projectRoleCheckPermission(ctx context.Context, resourceOwner string, grantID string, permissionCheck domain.PermissionCheck) error { + return permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, grantID) +} + +func projectRolePermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectRoleSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + ProjectRoleColumnResourceOwner, + domain.PermissionProjectRoleRead, + SingleOrgPermissionOption(queries.Queries), + WithProjectsPermissionOption(ProjectRoleColumnProjectID), + ) + return query.JoinClause(join, args...) +} + +func (q *Queries) SearchProjectRoles(ctx context.Context, shouldTriggerBulk bool, queries *ProjectRoleSearchQueries, permissionCheck domain.PermissionCheck) (roles *ProjectRoles, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + projectRoles, err := q.searchProjectRoles(ctx, shouldTriggerBulk, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + projectRolesCheckPermission(ctx, projectRoles, permissionCheck) + } + return projectRoles, nil +} + +func (q *Queries) searchProjectRoles(ctx context.Context, shouldTriggerBulk bool, queries *ProjectRoleSearchQueries, permissionCheckV2 bool) (roles *ProjectRoles, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -94,6 +134,7 @@ func (q *Queries) SearchProjectRoles(ctx context.Context, shouldTriggerBulk bool eq := sq.Eq{ProjectRoleColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} query, scan := prepareProjectRolesQuery() + query = projectRolePermissionCheckV2(ctx, query, permissionCheckV2, queries) stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-3N9ff", "Errors.Query.InvalidRequest") diff --git a/internal/query/projection/authn_key.go b/internal/query/projection/authn_key.go index e2229ad332..a287701cfb 100644 --- a/internal/query/projection/authn_key.go +++ b/internal/query/projection/authn_key.go @@ -62,6 +62,9 @@ func (*authNKeyProjection) Init() *old_handler.Check { handler.NewPrimaryKey(AuthNKeyInstanceIDCol, AuthNKeyIDCol), handler.WithIndex(handler.NewIndex("enabled", []string{AuthNKeyEnabledCol})), handler.WithIndex(handler.NewIndex("identifier", []string{AuthNKeyIdentifierCol})), + handler.WithIndex(handler.NewIndex("resource_owner", []string{AuthNKeyResourceOwnerCol})), + handler.WithIndex(handler.NewIndex("creation_date", []string{AuthNKeyCreationDateCol})), + handler.WithIndex(handler.NewIndex("expiration_date", []string{AuthNKeyExpirationCol})), ), ) } diff --git a/internal/query/projection/execution.go b/internal/query/projection/execution.go index 9001fcd3ba..1bd7f2e7f5 100644 --- a/internal/query/projection/execution.go +++ b/internal/query/projection/execution.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/handler/v2" exec "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/target" ) const ( @@ -78,6 +79,15 @@ func (p *executionProjection) Reducers() []handler.AggregateReducer { }, }, }, + { + Aggregate: target.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: target.RemovedEventType, + Reduce: p.reduceTargetRemoved, + }, + }, + }, { Aggregate: instance.AggregateType, EventReducers: []handler.EventReducer{ @@ -152,6 +162,21 @@ func (p *executionProjection) reduceExecutionSet(event eventstore.Event) (*handl return handler.NewMultiStatement(e, stmts...), nil } +func (p *executionProjection) reduceTargetRemoved(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*target.RemovedEvent](event) + if err != nil { + return nil, err + } + return handler.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(ExecutionTargetInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCond(ExecutionTargetTargetIDCol, e.Aggregate().ID), + }, + handler.WithTableSuffix(ExecutionTargetSuffix), + ), nil +} + func (p *executionProjection) reduceExecutionRemoved(event eventstore.Event) (*handler.Statement, error) { e, err := assertEvent[*exec.RemovedEvent](event) if err != nil { diff --git a/internal/query/projection/execution_test.go b/internal/query/projection/execution_test.go index 27d6e89258..aecae6905a 100644 --- a/internal/query/projection/execution_test.go +++ b/internal/query/projection/execution_test.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/handler/v2" exec "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/target" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -79,6 +80,35 @@ func TestExecutionProjection_reduces(t *testing.T) { }, }, }, + { + name: "reduceTargetRemoved", + args: args{ + event: getEvent( + testEvent( + target.RemovedEventType, + target.AggregateType, + []byte(`{}`), + ), + eventstore.GenericEventMapper[target.RemovedEvent], + ), + }, + reduce: (&executionProjection{}).reduceTargetRemoved, + want: wantReduce{ + aggregateType: eventstore.AggregateType("target"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.executions1_targets WHERE (instance_id = $1) AND (target_id = $2)", + expectedArgs: []interface{}{ + "instance-id", + "agg-id", + }, + }, + }, + }, + }, + }, { name: "reduceExecutionRemoved", args: args{ diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index 55c74b851c..11eeb8c613 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -173,6 +173,7 @@ const ( SAMLWithSignedRequestCol = "with_signed_request" SAMLNameIDFormatCol = "name_id_format" SAMLTransientMappingAttributeName = "transient_mapping_attribute_name" + SAMLFederatedLogoutEnabled = "federated_logout_enabled" ) type idpTemplateProjection struct{} @@ -377,6 +378,7 @@ func (*idpTemplateProjection) Init() *old_handler.Check { handler.NewColumn(SAMLWithSignedRequestCol, handler.ColumnTypeBool, handler.Nullable()), handler.NewColumn(SAMLNameIDFormatCol, handler.ColumnTypeEnum, handler.Nullable()), handler.NewColumn(SAMLTransientMappingAttributeName, handler.ColumnTypeText, handler.Nullable()), + handler.NewColumn(SAMLFederatedLogoutEnabled, handler.ColumnTypeBool, handler.Default(false)), }, handler.NewPrimaryKey(SAMLInstanceIDCol, SAMLIDCol), IDPTemplateSAMLSuffix, @@ -1990,6 +1992,7 @@ func (p *idpTemplateProjection) reduceSAMLIDPAdded(event eventstore.Event) (*han handler.NewCol(SAMLBindingCol, idpEvent.Binding), handler.NewCol(SAMLWithSignedRequestCol, idpEvent.WithSignedRequest), handler.NewCol(SAMLTransientMappingAttributeName, idpEvent.TransientMappingAttributeName), + handler.NewCol(SAMLFederatedLogoutEnabled, idpEvent.FederatedLogoutEnabled), } if idpEvent.NameIDFormat != nil { columns = append(columns, handler.NewCol(SAMLNameIDFormatCol, *idpEvent.NameIDFormat)) @@ -2525,5 +2528,8 @@ func reduceSAMLIDPChangedColumns(idpEvent idp.SAMLIDPChangedEvent) []handler.Col if idpEvent.TransientMappingAttributeName != nil { SAMLCols = append(SAMLCols, handler.NewCol(SAMLTransientMappingAttributeName, *idpEvent.TransientMappingAttributeName)) } + if idpEvent.FederatedLogoutEnabled != nil { + SAMLCols = append(SAMLCols, handler.NewCol(SAMLFederatedLogoutEnabled, *idpEvent.FederatedLogoutEnabled)) + } return SAMLCols } diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go index cebf3f8791..6ba6dabd47 100644 --- a/internal/query/projection/idp_template_test.go +++ b/internal/query/projection/idp_template_test.go @@ -2793,7 +2793,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { "isLinkingAllowed": true, "isAutoCreation": true, "isAutoUpdate": true, - "autoLinkingOption": 1 + "autoLinkingOption": 1, + "federatedLogoutEnabled": true }`), ), instance.SAMLIDPAddedEventMapper), }, @@ -2824,7 +2825,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, federated_logout_enabled, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "idp-id", "instance-id", @@ -2834,6 +2835,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { "binding", true, "customAttribute", + true, domain.SAMLNameIDFormatTransient, }, }, @@ -2865,7 +2867,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { "isLinkingAllowed": true, "isAutoCreation": true, "isAutoUpdate": true, - "autoLinkingOption": 1 + "autoLinkingOption": 1, + "federatedLogoutEnabled": true }`), ), org.SAMLIDPAddedEventMapper), }, @@ -2896,7 +2899,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, federated_logout_enabled, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "idp-id", "instance-id", @@ -2906,6 +2909,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { "binding", true, "customAttribute", + true, domain.SAMLNameIDFormatTransient, }, }, @@ -2976,7 +2980,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { "isLinkingAllowed": true, "isAutoCreation": true, "isAutoUpdate": true, - "autoLinkingOption": 1 + "autoLinkingOption": 1, + "federatedLogoutEnabled": true }`), ), instance.SAMLIDPChangedEventMapper), }, @@ -3002,13 +3007,14 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.idp_templates6_saml SET (metadata, key, certificate, binding, with_signed_request) = ($1, $2, $3, $4, $5) WHERE (idp_id = $6) AND (instance_id = $7)", + expectedStmt: "UPDATE projections.idp_templates6_saml SET (metadata, key, certificate, binding, with_signed_request, federated_logout_enabled) = ($1, $2, $3, $4, $5, $6) WHERE (idp_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ []byte("metadata"), anyArg{}, anyArg{}, "binding", true, + true, "idp-id", "instance-id", }, diff --git a/internal/query/projection/login_name.go b/internal/query/projection/login_name.go index 3c31928af4..e60f725dc7 100644 --- a/internal/query/projection/login_name.go +++ b/internal/query/projection/login_name.go @@ -2,9 +2,7 @@ package projection import ( "context" - "strings" - - sq "github.com/Masterminds/squirrel" + _ "embed" "github.com/zitadel/zitadel/internal/eventstore" old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" @@ -58,105 +56,8 @@ const ( LoginNamePoliciesInstanceIDCol = "instance_id" ) -var ( - policyUsers = sq.Select( - alias( - col(usersAlias, LoginNameUserIDCol), - LoginNameUserCol, - ), - col(usersAlias, LoginNameUserUserNameCol), - col(usersAlias, LoginNameUserInstanceIDCol), - col(usersAlias, LoginNameUserResourceOwnerCol), - alias( - coalesce(col(policyCustomAlias, LoginNamePoliciesMustBeDomainCol), col(policyDefaultAlias, LoginNamePoliciesMustBeDomainCol)), - LoginNamePoliciesMustBeDomainCol, - ), - ).From(alias(LoginNameUserProjectionTable, usersAlias)). - LeftJoin( - leftJoin(LoginNamePolicyProjectionTable, policyCustomAlias, - eq(col(policyCustomAlias, LoginNamePoliciesResourceOwnerCol), col(usersAlias, LoginNameUserResourceOwnerCol)), - eq(col(policyCustomAlias, LoginNamePoliciesInstanceIDCol), col(usersAlias, LoginNameUserInstanceIDCol)), - ), - ). - LeftJoin( - leftJoin(LoginNamePolicyProjectionTable, policyDefaultAlias, - eq(col(policyDefaultAlias, LoginNamePoliciesIsDefaultCol), "true"), - eq(col(policyDefaultAlias, LoginNamePoliciesInstanceIDCol), col(usersAlias, LoginNameUserInstanceIDCol)), - ), - ) - - loginNamesTable = sq.Select( - col(policyUsersAlias, LoginNameUserCol), - col(policyUsersAlias, LoginNameUserUserNameCol), - col(policyUsersAlias, LoginNameUserResourceOwnerCol), - alias(col(policyUsersAlias, LoginNameUserInstanceIDCol), - LoginNameInstanceIDCol), - col(policyUsersAlias, LoginNamePoliciesMustBeDomainCol), - alias(col(domainsAlias, LoginNameDomainNameCol), - domainAlias), - col(domainsAlias, LoginNameDomainIsPrimaryCol), - ).FromSelect(policyUsers, policyUsersAlias). - LeftJoin( - leftJoin(LoginNameDomainProjectionTable, domainsAlias, - col(policyUsersAlias, LoginNamePoliciesMustBeDomainCol), - eq(col(policyUsersAlias, LoginNameUserResourceOwnerCol), col(domainsAlias, LoginNameDomainResourceOwnerCol)), - eq(col(policyUsersAlias, LoginNamePoliciesInstanceIDCol), col(domainsAlias, LoginNameDomainInstanceIDCol)), - ), - ) - - viewStmt, _ = sq.Select( - LoginNameUserCol, - alias( - whenThenElse( - LoginNamePoliciesMustBeDomainCol, - concat(LoginNameUserUserNameCol, "'@'", domainAlias), - LoginNameUserUserNameCol), - LoginNameCol), - alias(coalesce(LoginNameDomainIsPrimaryCol, "true"), - LoginNameIsPrimaryCol), - LoginNameInstanceIDCol, - ).FromSelect(loginNamesTable, LoginNameTableAlias).MustSql() -) - -func col(table, name string) string { - return table + "." + name -} - -func alias(col, alias string) string { - return col + " AS " + alias -} - -func coalesce(values ...string) string { - str := "COALESCE(" - for i, value := range values { - if i > 0 { - str += ", " - } - str += value - } - str += ")" - return str -} - -func eq(first, second string) string { - return first + " = " + second -} - -func leftJoin(table, alias, on string, and ...string) string { - st := table + " " + alias + " ON " + on - for _, a := range and { - st += " AND " + a - } - return st -} - -func concat(strs ...string) string { - return "CONCAT(" + strings.Join(strs, ", ") + ")" -} - -func whenThenElse(when, then, el string) string { - return "(CASE WHEN " + when + " THEN " + then + " ELSE " + el + " END)" -} +//go:embed login_name_query.sql +var loginNameViewStmt string type loginNameProjection struct{} @@ -170,7 +71,7 @@ func (*loginNameProjection) Name() string { func (*loginNameProjection) Init() *old_handler.Check { return handler.NewViewCheck( - viewStmt, + loginNameViewStmt, handler.NewSuffixedTable( []*handler.InitColumn{ handler.NewColumn(LoginNameUserIDCol, handler.ColumnTypeText), @@ -229,7 +130,9 @@ func (*loginNameProjection) Init() *old_handler.Check { }, handler.NewPrimaryKey(LoginNamePoliciesInstanceIDCol, LoginNamePoliciesResourceOwnerCol), loginNamePolicySuffix, - handler.WithIndex(handler.NewIndex("is_default", []string{LoginNamePoliciesResourceOwnerCol, LoginNamePoliciesIsDefaultCol})), + // this index is not used anymore, but kept for understanding why the default exists on existing systems, TODO: remove in login_names4 + // handler.WithIndex(handler.NewIndex("is_default", []string{LoginNamePoliciesResourceOwnerCol, LoginNamePoliciesIsDefaultCol})), + handler.WithIndex(handler.NewIndex("is_default_owner", []string{LoginNamePoliciesInstanceIDCol, LoginNamePoliciesIsDefaultCol, LoginNamePoliciesResourceOwnerCol}, handler.WithInclude(LoginNamePoliciesMustBeDomainCol))), ), ) } diff --git a/internal/query/projection/login_name_query.sql b/internal/query/projection/login_name_query.sql new file mode 100644 index 0000000000..89dc803feb --- /dev/null +++ b/internal/query/projection/login_name_query.sql @@ -0,0 +1,35 @@ +SELECT + u.id AS user_id + , CASE + WHEN p.must_be_domain THEN CONCAT(u.user_name, '@', d.name) + ELSE u.user_name + END AS login_name + , COALESCE(d.is_primary, TRUE) AS is_primary + , u.instance_id +FROM + projections.login_names3_users AS u +LEFT JOIN LATERAL ( + SELECT + must_be_domain + , is_default + FROM + projections.login_names3_policies AS p + WHERE + ( + p.instance_id = u.instance_id + AND NOT p.is_default + AND p.resource_owner = u.resource_owner + ) OR ( + p.instance_id = u.instance_id + AND p.is_default + ) + ORDER BY + p.is_default -- custom first + LIMIT 1 +) AS p ON TRUE +LEFT JOIN + projections.login_names3_domains d + ON + p.must_be_domain + AND u.resource_owner = d.resource_owner + AND u.instance_id = d.instance_id \ No newline at end of file diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index f4e3bbe0d4..77a28ac79a 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -2,6 +2,11 @@ package projection import ( "context" + "errors" + "fmt" + + "github.com/jackc/pgx/v5/pgconn" + "github.com/zitadel/logging" internal_authz "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" @@ -90,6 +95,7 @@ var ( ) type projection interface { + ProjectionName() string Start(ctx context.Context) Init(ctx context.Context) error Trigger(ctx context.Context, opts ...handler.TriggerOpt) (_ context.Context, err error) @@ -206,21 +212,41 @@ func Start(ctx context.Context) { } func ProjectInstance(ctx context.Context) error { - for _, projection := range projections { - _, err := projection.Trigger(ctx) - if err != nil { - return err + for i, projection := range projections { + logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting projection") + for { + _, err := projection.Trigger(ctx) + if err == nil { + break + } + var pgErr *pgconn.PgError + errors.As(err, &pgErr) + if pgErr.Code != database.PgUniqueConstraintErrorCode { + return err + } + logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("projection failed because of unique constraint, retrying") } + logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("projection done") } return nil } func ProjectInstanceFields(ctx context.Context) error { - for _, fieldProjection := range fields { - err := fieldProjection.Trigger(ctx) - if err != nil { - return err + for i, fieldProjection := range fields { + logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(fields))).Info("starting fields projection") + for { + err := fieldProjection.Trigger(ctx) + if err == nil { + break + } + var pgErr *pgconn.PgError + errors.As(err, &pgErr) + if pgErr.Code != database.PgUniqueConstraintErrorCode { + return err + } + logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("fields projection failed because of unique constraint, retrying") } + logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(fields))).Info("fields projection done") } return nil } @@ -249,6 +275,10 @@ func applyCustomConfig(config handler.Config, customConfig CustomConfig) handler return config } +// 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 +// will be refactored when changing to new id based projections func newFieldsList() { fields = []*handler.FieldHandler{ ProjectGrantFields, diff --git a/internal/query/projection/session.go b/internal/query/projection/session.go index 196a352190..53eba54efb 100644 --- a/internal/query/projection/session.go +++ b/internal/query/projection/session.go @@ -87,6 +87,7 @@ func (*sessionProjection) Init() *old_handler.Check { SessionColumnUserAgentFingerprintID+"_idx", []string{SessionColumnUserAgentFingerprintID}, )), + handler.WithIndex(handler.NewIndex(SessionColumnUserID+"_idx", []string{SessionColumnUserID})), ), ) } diff --git a/internal/query/projection/user.go b/internal/query/projection/user.go index f1e0613287..d11c4855f7 100644 --- a/internal/query/projection/user.go +++ b/internal/query/projection/user.go @@ -124,6 +124,7 @@ func (*userProjection) Init() *old_handler.Check { handler.NewPrimaryKey(HumanUserInstanceIDCol, HumanUserIDCol), UserHumanSuffix, handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()), + handler.WithIndex(handler.NewIndex("email", []string{HumanUserInstanceIDCol, "LOWER(" + HumanEmailCol + ")"})), ), handler.NewSuffixedTable([]*handler.InitColumn{ handler.NewColumn(MachineUserIDCol, handler.ColumnTypeText), diff --git a/internal/query/projection/user_auth_method.go b/internal/query/projection/user_auth_method.go index b986df1558..7726550ffd 100644 --- a/internal/query/projection/user_auth_method.go +++ b/internal/query/projection/user_auth_method.go @@ -125,6 +125,10 @@ func (p *userAuthMethodProjection) Reducers() []handler.AggregateReducer { Event: user.HumanOTPEmailRemovedType, Reduce: p.reduceRemoveAuthMethod, }, + { + Event: user.UserRemovedType, + Reduce: p.reduceUserRemoved, + }, }, }, { @@ -311,3 +315,18 @@ func (p *userAuthMethodProjection) reduceOwnerRemoved(event eventstore.Event) (* }, ), nil } + +func (p *userAuthMethodProjection) reduceUserRemoved(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*user.UserRemovedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-FwDZ8", "reduce.wrong.event.type %s", user.UserRemovedType) + } + return handler.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(UserAuthMethodInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCond(UserAuthMethodResourceOwnerCol, e.Aggregate().ResourceOwner), + handler.NewCond(UserAuthMethodUserIDCol, e.Aggregate().ID), + }, + ), nil +} diff --git a/internal/query/projection/user_auth_method_test.go b/internal/query/projection/user_auth_method_test.go index fb3d6d9d91..e21a480a9d 100644 --- a/internal/query/projection/user_auth_method_test.go +++ b/internal/query/projection/user_auth_method_test.go @@ -528,6 +528,34 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { }, }, }, + { + name: "reduceUserRemoved", + reduce: (&userAuthMethodProjection{}).reduceUserRemoved, + args: args{ + event: getEvent( + testEvent( + user.UserRemovedType, + user.AggregateType, + nil, + ), user.UserRemovedEventMapper), + }, + want: wantReduce{ + aggregateType: eventstore.AggregateType("user"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.user_auth_methods5 WHERE (instance_id = $1) AND (resource_owner = $2) AND (user_id = $3)", + expectedArgs: []interface{}{ + "instance-id", + "ro-id", + "agg-id", + }, + }, + }, + }, + }, + }, { name: "org reduceOwnerRemoved", reduce: (&userAuthMethodProjection{}).reduceOwnerRemoved, diff --git a/internal/query/projection/user_personal_access_token.go b/internal/query/projection/user_personal_access_token.go index 0efb5d6412..610ca9c4e2 100644 --- a/internal/query/projection/user_personal_access_token.go +++ b/internal/query/projection/user_personal_access_token.go @@ -56,6 +56,8 @@ func (*personalAccessTokenProjection) Init() *old_handler.Check { handler.WithIndex(handler.NewIndex("user_id", []string{PersonalAccessTokenColumnUserID})), handler.WithIndex(handler.NewIndex("resource_owner", []string{PersonalAccessTokenColumnResourceOwner})), handler.WithIndex(handler.NewIndex("owner_removed", []string{PersonalAccessTokenColumnOwnerRemoved})), + handler.WithIndex(handler.NewIndex("creation_date", []string{PersonalAccessTokenColumnCreationDate})), + handler.WithIndex(handler.NewIndex("expiration_date", []string{PersonalAccessTokenColumnExpiration})), ), ) } diff --git a/internal/query/resource_counts.go b/internal/query/resource_counts.go new file mode 100644 index 0000000000..9d486e0b90 --- /dev/null +++ b/internal/query/resource_counts.go @@ -0,0 +1,61 @@ +package query + +import ( + "context" + "database/sql" + _ "embed" + "time" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +var ( + //go:embed resource_counts_list.sql + resourceCountsListQuery string +) + +type ResourceCount struct { + ID int // Primary key, used for pagination + InstanceID string + TableName string + ParentType domain.CountParentType + ParentID string + Resource string + UpdatedAt time.Time + Amount int +} + +// ListResourceCounts retrieves all resource counts. +// It supports pagination using lastID and limit parameters. +// +// TODO: Currently only a proof of concept, filters may be implemented later if required. +func (q *Queries) ListResourceCounts(ctx context.Context, lastID, limit int) (result []ResourceCount, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + for rows.Next() { + var count ResourceCount + err := rows.Scan( + &count.ID, + &count.InstanceID, + &count.TableName, + &count.ParentType, + &count.ParentID, + &count.Resource, + &count.UpdatedAt, + &count.Amount) + if err != nil { + return zerrors.ThrowInternal(err, "QUERY-2f4g5", "Errors.Internal") + } + result = append(result, count) + } + return nil + }, resourceCountsListQuery, lastID, limit) + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-3f4g5", "Errors.Internal") + } + return result, nil +} diff --git a/internal/query/resource_counts_list.sql b/internal/query/resource_counts_list.sql new file mode 100644 index 0000000000..0d4abf87eb --- /dev/null +++ b/internal/query/resource_counts_list.sql @@ -0,0 +1,12 @@ +SELECT id, + instance_id, + table_name, + parent_type, + parent_id, + resource_name, + updated_at, + amount +FROM projections.resource_counts +WHERE id > $1 +ORDER BY id +LIMIT $2; diff --git a/internal/query/resource_counts_test.go b/internal/query/resource_counts_test.go new file mode 100644 index 0000000000..2829a660ef --- /dev/null +++ b/internal/query/resource_counts_test.go @@ -0,0 +1,109 @@ +package query + +import ( + "context" + _ "embed" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" +) + +func TestQueries_ListResourceCounts(t *testing.T) { + columns := []string{"id", "instance_id", "table_name", "parent_type", "parent_id", "resource_name", "updated_at", "amount"} + type args struct { + lastID int + limit int + } + tests := []struct { + name string + args args + expects func(sqlmock.Sqlmock) + wantResult []ResourceCount + wantErr bool + }{ + { + name: "query error", + args: args{ + lastID: 0, + limit: 10, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(regexp.QuoteMeta(resourceCountsListQuery)). + WithArgs(0, 10). + WillReturnError(assert.AnError) + }, + wantErr: true, + }, + { + name: "success", + args: args{ + lastID: 0, + limit: 10, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(regexp.QuoteMeta(resourceCountsListQuery)). + WithArgs(0, 10). + WillReturnRows( + sqlmock.NewRows(columns). + AddRow(1, "instance_1", "table", "instance", "parent_1", "resource_name", time.Unix(1, 2), 5). + AddRow(2, "instance_2", "table", "instance", "parent_2", "resource_name", time.Unix(1, 2), 6), + ) + }, + wantResult: []ResourceCount{ + { + ID: 1, + InstanceID: "instance_1", + TableName: "table", + ParentType: domain.CountParentTypeInstance, + ParentID: "parent_1", + Resource: "resource_name", + UpdatedAt: time.Unix(1, 2), + Amount: 5, + }, + { + ID: 2, + InstanceID: "instance_2", + TableName: "table", + ParentType: domain.CountParentTypeInstance, + ParentID: "parent_2", + Resource: "resource_name", + UpdatedAt: time.Unix(1, 2), + Amount: 6, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer func() { + err := mock.ExpectationsWereMet() + require.NoError(t, err) + }() + defer db.Close() + tt.expects(mock) + mock.ExpectClose() + q := &Queries{ + client: &database.DB{ + DB: db, + }, + } + + gotResult, err := q.ListResourceCounts(context.Background(), tt.args.lastID, tt.args.limit) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantResult, gotResult, "ListResourceCounts() result mismatch") + }) + } +} diff --git a/internal/query/restrictions.go b/internal/query/restrictions.go index 8cff5737f7..93d435278c 100644 --- a/internal/query/restrictions.go +++ b/internal/query/restrictions.go @@ -78,7 +78,7 @@ func (q *Queries) GetInstanceRestrictions(ctx context.Context) (restrictions Res RestrictionsColumnResourceOwner.identifier(): instanceID, }).ToSql() if err != nil { - return restrictions, zitade_errors.ThrowInternal(err, "QUERY-XnLMQ", "Errors.Query.SQLStatment") + return restrictions, zitade_errors.ThrowInternal(err, "QUERY-XnLMQ", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { restrictions, err = scan(row) diff --git a/internal/query/saml_request_test.go b/internal/query/saml_request_test.go index 3a062ac5fd..019054d4fc 100644 --- a/internal/query/saml_request_test.go +++ b/internal/query/saml_request_test.go @@ -5,7 +5,6 @@ import ( "database/sql" "database/sql/driver" _ "embed" - "fmt" "regexp" "testing" @@ -20,9 +19,7 @@ import ( ) func TestQueries_SamlRequestByID(t *testing.T) { - expQuery := regexp.QuoteMeta(fmt.Sprintf( - samlRequestByIDQuery, - )) + expQuery := regexp.QuoteMeta(samlRequestByIDQuery) cols := []string{ projection.SamlRequestColumnID, diff --git a/internal/query/search_query.go b/internal/query/search_query.go index d5e09027c4..d6dd710d1e 100644 --- a/internal/query/search_query.go +++ b/internal/query/search_query.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "reflect" + "strings" "time" sq "github.com/Masterminds/squirrel" @@ -334,23 +335,23 @@ func (q *textQuery) comp() sq.Sqlizer { case TextNotEquals: return sq.NotEq{q.Column.identifier(): q.Text} case TextEqualsIgnoreCase: - return sq.ILike{q.Column.identifier(): q.Text} + return sq.Like{"LOWER(" + q.Column.identifier() + ")": strings.ToLower(q.Text)} case TextNotEqualsIgnoreCase: - return sq.NotILike{q.Column.identifier(): q.Text} + return sq.NotLike{"LOWER(" + q.Column.identifier() + ")": strings.ToLower(q.Text)} case TextStartsWith: return sq.Like{q.Column.identifier(): q.Text + "%"} case TextStartsWithIgnoreCase: - return sq.ILike{q.Column.identifier(): q.Text + "%"} + return sq.Like{"LOWER(" + q.Column.identifier() + ")": strings.ToLower(q.Text) + "%"} case TextEndsWith: return sq.Like{q.Column.identifier(): "%" + q.Text} case TextEndsWithIgnoreCase: - return sq.ILike{q.Column.identifier(): "%" + q.Text} + return sq.Like{"LOWER(" + q.Column.identifier() + ")": "%" + strings.ToLower(q.Text)} case TextContains: return sq.Like{q.Column.identifier(): "%" + q.Text + "%"} case TextContainsIgnoreCase: - return sq.ILike{q.Column.identifier(): "%" + q.Text + "%"} + return sq.Like{"LOWER(" + q.Column.identifier() + ")": "%" + strings.ToLower(q.Text) + "%"} case TextListContains: - return &listContains{col: q.Column, args: []interface{}{q.Text}} + return &listContains{col: q.Column, args: []any{q.Text}} case textCompareMax: return nil } diff --git a/internal/query/search_query_test.go b/internal/query/search_query_test.go index 13142a0158..7f6672b279 100644 --- a/internal/query/search_query_test.go +++ b/internal/query/search_query_test.go @@ -1204,7 +1204,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextEqualsIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "Hurst"}, + query: sq.Like{"LOWER(test_table.test_col)": "hurst"}, }, }, { @@ -1226,7 +1226,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextNotEqualsIgnoreCase, }, want: want{ - query: sq.NotILike{"test_table.test_col": "Hurst"}, + query: sq.NotLike{"LOWER(test_table.test_col)": "hurst"}, }, }, { @@ -1237,7 +1237,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextEqualsIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "Hu\\%\\%rst"}, + query: sq.Like{"LOWER(test_table.test_col)": "hu\\%\\%rst"}, }, }, { @@ -1270,7 +1270,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextStartsWithIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "Hurst%"}, + query: sq.Like{"LOWER(test_table.test_col)": "hurst%"}, }, }, { @@ -1281,7 +1281,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextStartsWithIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "Hurst\\%%"}, + query: sq.Like{"LOWER(test_table.test_col)": "hurst\\%%"}, }, }, { @@ -1314,7 +1314,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextEndsWithIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "%Hurst"}, + query: sq.Like{"LOWER(test_table.test_col)": "%hurst"}, }, }, { @@ -1325,7 +1325,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextEndsWithIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "%\\%Hurst"}, + query: sq.Like{"LOWER(test_table.test_col)": "%\\%hurst"}, }, }, { @@ -1351,14 +1351,14 @@ func TestTextQuery_comp(t *testing.T) { }, }, { - name: "containts ignore case", + name: "contains ignore case", fields: fields{ Column: testCol, Text: "Hurst", Compare: TextContainsIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "%Hurst%"}, + query: sq.Like{"LOWER(test_table.test_col)": "%hurst%"}, }, }, { @@ -1369,11 +1369,11 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextContainsIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "%\\%Hurst\\%%"}, + query: sq.Like{"LOWER(test_table.test_col)": "%\\%hurst\\%%"}, }, }, { - name: "list containts", + name: "list contains", fields: fields{ Column: testCol, Text: "Hurst", diff --git a/internal/query/secret_generators.go b/internal/query/secret_generators.go index c267d7b290..ca77bc35b5 100644 --- a/internal/query/secret_generators.go +++ b/internal/query/secret_generators.go @@ -132,7 +132,7 @@ func (q *Queries) SecretGeneratorByType(ctx context.Context, generatorType domai SecretGeneratorColumnInstanceID.identifier(): instanceID, }).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-3k99f", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-3k99f", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/security_policy.go b/internal/query/security_policy.go index 7a3fb3fa89..5a2450258e 100644 --- a/internal/query/security_policy.go +++ b/internal/query/security_policy.go @@ -67,7 +67,7 @@ func (q *Queries) SecurityPolicy(ctx context.Context) (policy *SecurityPolicy, e SecurityPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Sf6d1", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Sf6d1", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/user.go b/internal/query/user.go index a97e3bbd14..ac3eb79fc9 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -132,16 +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 { +func userPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, filters []SearchQuery) sq.SelectBuilder { + return userPermissionCheckV2WithCustomColumns(ctx, query, enabled, filters, UserResourceOwnerCol, UserIDCol) +} + +func userPermissionCheckV2WithCustomColumns(ctx context.Context, query sq.SelectBuilder, enabled bool, filters []SearchQuery, userResourceOwnerCol, userID Column) sq.SelectBuilder { if !enabled { return query } join, args := PermissionClause( ctx, - UserResourceOwnerCol, + userResourceOwnerCol, domain.PermissionUserRead, - SingleOrgPermissionOption(queries.Queries), - OwnedRowsPermissionOption(UserIDCol), + SingleOrgPermissionOption(filters), + OwnedRowsPermissionOption(userID), ) return query.JoinClause(join, args...) } @@ -196,21 +200,15 @@ var ( userLoginNamesTable = loginNameTable.setAlias("login_names") userLoginNamesUserIDCol = LoginNameUserIDCol.setTable(userLoginNamesTable) - userLoginNamesNameCol = LoginNameNameCol.setTable(userLoginNamesTable) userLoginNamesInstanceIDCol = LoginNameInstanceIDCol.setTable(userLoginNamesTable) userLoginNamesListCol = Column{ - name: "loginnames", + name: "login_names", table: userLoginNamesTable, } - userLoginNamesLowerListCol = Column{ - name: "loginnames_lower", + userPreferredLoginNameCol = Column{ + name: "preferred_login_name", table: userLoginNamesTable, } - userPreferredLoginNameTable = loginNameTable.setAlias("preferred_login_name") - userPreferredLoginNameUserIDCol = LoginNameUserIDCol.setTable(userPreferredLoginNameTable) - userPreferredLoginNameCol = LoginNameNameCol.setTable(userPreferredLoginNameTable) - userPreferredLoginNameIsPrimaryCol = LoginNameIsPrimaryCol.setTable(userPreferredLoginNameTable) - userPreferredLoginNameInstanceIDCol = LoginNameInstanceIDCol.setTable(userPreferredLoginNameTable) ) var ( @@ -455,7 +453,7 @@ func (q *Queries) GetHumanProfile(ctx context.Context, userID string, queries .. } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -479,7 +477,7 @@ func (q *Queries) GetHumanEmail(ctx context.Context, userID string, queries ...S } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-BHhj3", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-BHhj3", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -503,7 +501,7 @@ func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...S } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -589,7 +587,7 @@ func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, queri } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -607,7 +605,7 @@ func (q *Queries) CountUsers(ctx context.Context, queries *UserSearchQueries) (c eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { - return 0, zerrors.ThrowInternal(err, "QUERY-w3Dx", "Errors.Query.SQLStatment") + return 0, zerrors.ThrowInternal(err, "QUERY-w3Dx", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { @@ -637,12 +635,12 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, p defer func() { span.EndWithError(err) }() query, scan := prepareUsersQuery() - query = userPermissionCheckV2(ctx, query, permissionCheckV2, queries) + query = userPermissionCheckV2(ctx, query, permissionCheckV2, queries.Queries) stmt, args, err := queries.toQuery(query).Where(sq.Eq{ UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { @@ -689,7 +687,7 @@ func (q *Queries) IsUserUnique(ctx context.Context, username, email, resourceOwn eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := query.Where(eq).ToSql() if err != nil { - return false, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatment") + return false, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -776,11 +774,11 @@ func NewUserVerifiedPhoneSearchQuery(value string, comparison TextComparison) (S return NewTextQuery(NotifyVerifiedPhoneCol, value, comparison) } -func NewUserStateSearchQuery(value int32) (SearchQuery, error) { +func NewUserStateSearchQuery(value domain.UserState) (SearchQuery, error) { return NewNumberQuery(UserStateCol, value, NumberEquals) } -func NewUserTypeSearchQuery(value int32) (SearchQuery, error) { +func NewUserTypeSearchQuery(value domain.UserType) (SearchQuery, error) { return NewNumberQuery(UserTypeCol, value, NumberEquals) } @@ -788,12 +786,8 @@ func NewUserPreferredLoginNameSearchQuery(value string, comparison TextCompariso return NewTextQuery(userPreferredLoginNameCol, value, comparison) } -func NewUserLoginNamesSearchQuery(value string) (SearchQuery, error) { - return NewTextQuery(userLoginNamesLowerListCol, strings.ToLower(value), TextListContains) -} - func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (SearchQuery, error) { - // linking queries for the subselect + // linking queries for the sub select instanceQuery, err := NewColumnComparisonQuery(LoginNameInstanceIDCol, UserInstanceIDCol, ColumnEquals) if err != nil { return nil, err @@ -824,30 +818,16 @@ func triggerUserProjections(ctx context.Context) { triggerBatch(ctx, projection.UserProjection, projection.LoginNameProjection) } -func prepareLoginNamesQuery() (string, []interface{}, error) { - return sq.Select( - userLoginNamesUserIDCol.identifier(), - "ARRAY_AGG("+userLoginNamesNameCol.identifier()+")::TEXT[] AS "+userLoginNamesListCol.name, - "ARRAY_AGG(LOWER("+userLoginNamesNameCol.identifier()+"))::TEXT[] AS "+userLoginNamesLowerListCol.name, - userLoginNamesInstanceIDCol.identifier(), - ).From(userLoginNamesTable.identifier()). - GroupBy( - userLoginNamesUserIDCol.identifier(), - userLoginNamesInstanceIDCol.identifier(), - ).ToSql() -} - -func preparePreferredLoginNamesQuery() (string, []interface{}, error) { - return sq.Select( - userPreferredLoginNameUserIDCol.identifier(), - userPreferredLoginNameCol.identifier(), - userPreferredLoginNameInstanceIDCol.identifier(), - ).From(userPreferredLoginNameTable.identifier()). - Where(sq.Eq{ - userPreferredLoginNameIsPrimaryCol.identifier(): true, - }, - ).ToSql() -} +var joinLoginNames = `LEFT JOIN LATERAL (` + + `SELECT` + + ` ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names,` + + ` MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name` + + ` FROM` + + ` projections.login_names3 AS ln` + + ` WHERE` + + ` ln.user_id = ` + UserIDCol.identifier() + + ` AND ln.instance_id = ` + UserInstanceIDCol.identifier() + + `) AS login_names ON TRUE` func scanUser(row *sql.Row) (*User, error) { u := new(User) @@ -947,64 +927,6 @@ func scanUser(row *sql.Row) (*User, error) { return u, nil } -func prepareUserQuery() (sq.SelectBuilder, func(*sql.Row) (*User, error)) { - loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - return sq.Select( - UserIDCol.identifier(), - UserCreationDateCol.identifier(), - UserChangeDateCol.identifier(), - UserResourceOwnerCol.identifier(), - UserSequenceCol.identifier(), - UserStateCol.identifier(), - UserTypeCol.identifier(), - UserUsernameCol.identifier(), - userLoginNamesListCol.identifier(), - userPreferredLoginNameCol.identifier(), - HumanUserIDCol.identifier(), - HumanFirstNameCol.identifier(), - HumanLastNameCol.identifier(), - HumanNickNameCol.identifier(), - HumanDisplayNameCol.identifier(), - HumanPreferredLanguageCol.identifier(), - HumanGenderCol.identifier(), - HumanAvatarURLCol.identifier(), - HumanEmailCol.identifier(), - HumanIsEmailVerifiedCol.identifier(), - HumanPhoneCol.identifier(), - HumanIsPhoneVerifiedCol.identifier(), - HumanPasswordChangeRequiredCol.identifier(), - HumanPasswordChangedCol.identifier(), - HumanMFAInitSkippedCol.identifier(), - MachineUserIDCol.identifier(), - MachineNameCol.identifier(), - MachineDescriptionCol.identifier(), - MachineSecretCol.identifier(), - MachineAccessTokenTypeCol.identifier(), - countColumn.identifier(), - ). - From(userTable.identifier()). - LeftJoin(join(HumanUserIDCol, UserIDCol)). - LeftJoin(join(MachineUserIDCol, UserIDCol)). - LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+ - userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - loginNamesArgs...). - LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ - userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - preferredLoginNameArgs...). - PlaceholderFormat(sq.Dollar), - - scanUser -} - func prepareProfileQuery() (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) { return sq.Select( UserIDCol.identifier(), @@ -1166,14 +1088,6 @@ func preparePhoneQuery() (sq.SelectBuilder, func(*sql.Row) (*Phone, error)) { } func prepareNotifyUserQuery() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { - loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } return sq.Select( UserIDCol.identifier(), UserCreationDateCol.identifier(), @@ -1204,14 +1118,7 @@ func prepareNotifyUserQuery() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, er From(userTable.identifier()). LeftJoin(join(HumanUserIDCol, UserIDCol)). LeftJoin(join(NotifyUserIDCol, UserIDCol)). - LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+ - userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - loginNamesArgs...). - LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ - userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - preferredLoginNameArgs...). + JoinClause(joinLoginNames). PlaceholderFormat(sq.Dollar), scanNotifyUser } @@ -1355,14 +1262,6 @@ func prepareUserUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) { } func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { - loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } return sq.Select( UserIDCol.identifier(), UserCreationDateCol.identifier(), @@ -1397,14 +1296,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { From(userTable.identifier()). LeftJoin(join(HumanUserIDCol, UserIDCol)). LeftJoin(join(MachineUserIDCol, UserIDCol)). - LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+ - userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - loginNamesArgs...). - LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ - userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - preferredLoginNameArgs...). + JoinClause(joinLoginNames). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Users, error) { users := make([]*User, 0) diff --git a/internal/query/user_by_id.sql b/internal/query/user_by_id.sql index 2ce741f9b7..a89e701698 100644 --- a/internal/query/user_by_id.sql +++ b/internal/query/user_by_id.sql @@ -1,41 +1,3 @@ -WITH login_names AS (SELECT - u.id user_id - , u.instance_id - , u.resource_owner - , u.user_name - , d.name domain_name - , d.is_primary - , p.must_be_domain - , CASE WHEN p.must_be_domain - THEN concat(u.user_name, '@', d.name) - ELSE u.user_name - END login_name - FROM - projections.login_names3_users u - JOIN lateral ( - SELECT - p.must_be_domain - FROM - projections.login_names3_policies p - WHERE - u.instance_id = p.instance_id - AND ( - (p.is_default IS TRUE AND p.instance_id = $3) - OR (p.instance_id = $3 AND p.resource_owner = u.resource_owner) - ) - ORDER BY is_default - LIMIT 1 - ) p ON TRUE - JOIN - projections.login_names3_domains d - ON - u.instance_id = d.instance_id - AND u.resource_owner = d.resource_owner - WHERE - u.id = $1 - AND (u.resource_owner = $2 OR $2 = '') - AND u.instance_id = $3 -) SELECT u.id , u.creation_date @@ -45,8 +7,8 @@ SELECT , u.state , u.type , u.username - , (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln GROUP BY ln.user_id, ln.instance_id) login_names - , (SELECT ln.login_name login_names_lower FROM login_names ln WHERE ln.is_primary IS TRUE) preferred_login_name + , login_names.login_names AS login_names + , login_names.preferred_login_name AS preferred_login_name , h.user_id , h.first_name , h.last_name @@ -79,6 +41,16 @@ LEFT JOIN ON u.id = m.user_id AND u.instance_id = m.instance_id +LEFT JOIN LATERAL ( + SELECT + ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, + MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name + FROM + projections.login_names3 AS ln + WHERE + ln.user_id = u.id + AND ln.instance_id = u.instance_id +) AS login_names ON TRUE WHERE u.id = $1 AND (u.resource_owner = $2 OR $2 = '') diff --git a/internal/query/user_by_login_name.sql b/internal/query/user_by_login_name.sql index a37c213612..f7d2fd3e23 100644 --- a/internal/query/user_by_login_name.sql +++ b/internal/query/user_by_login_name.sql @@ -10,7 +10,7 @@ WITH found_users AS ( LEFT JOIN projections.login_names3_policies p_custom ON u.instance_id = p_custom.instance_id AND p_custom.instance_id = $4 AND p_custom.resource_owner = u.resource_owner - LEFT JOIN projections.login_names3_policies p_default + JOIN projections.login_names3_policies p_default ON u.instance_id = p_default.instance_id AND p_default.instance_id = $4 AND p_default.is_default IS TRUE AND ( diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index c3f24c066e..ebd4ab7c0c 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -280,7 +280,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh return nil, zerrors.ThrowInternal(err, "QUERY-wXnQR", "Errors.Query.SQLStatement") } - latestSequence, err := q.latestState(ctx, userGrantTable) + latestState, err := q.latestState(ctx, userGrantTable) if err != nil { return nil, err } @@ -293,7 +293,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh return nil, err } - grants.State = latestSequence + grants.State = latestState return grants, nil } diff --git a/internal/query/user_membership.go b/internal/query/user_membership.go index cae2b4dae3..cb7588624f 100644 --- a/internal/query/user_membership.go +++ b/internal/query/user_membership.go @@ -143,7 +143,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-T84X9", "Errors.Query.InvalidRequest") } - latestSequence, err := q.latestState(ctx, orgMemberTable, instanceMemberTable, projectMemberTable, projectGrantMemberTable) + latestState, err := q.latestState(ctx, orgMemberTable, instanceMemberTable, projectMemberTable, projectGrantMemberTable) if err != nil { return nil, err } @@ -156,7 +156,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer if err != nil { return nil, err } - memberships.State = latestSequence + memberships.State = latestState return memberships, nil } diff --git a/internal/query/user_metadata.go b/internal/query/user_metadata.go index ff612f82c8..534c707593 100644 --- a/internal/query/user_metadata.go +++ b/internal/query/user_metadata.go @@ -97,7 +97,7 @@ func (q *Queries) GetUserMetadataByKey(ctx context.Context, shouldTriggerBulk bo } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-aDGG2", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-aDGG2", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -125,7 +125,7 @@ func (q *Queries) SearchUserMetadataForUsers(ctx context.Context, shouldTriggerB } stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { @@ -157,7 +157,7 @@ func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool } stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { diff --git a/internal/query/user_notify_by_id.sql b/internal/query/user_notify_by_id.sql index 10aa60ee60..6322229a91 100644 --- a/internal/query/user_notify_by_id.sql +++ b/internal/query/user_notify_by_id.sql @@ -1,41 +1,3 @@ -WITH login_names AS ( - SELECT - u.id user_id - , u.instance_id - , u.resource_owner - , u.user_name - , d.name domain_name - , d.is_primary - , p.must_be_domain - , CASE WHEN p.must_be_domain - THEN concat(u.user_name, '@', d.name) - ELSE u.user_name - END login_name - FROM - projections.login_names3_users u - JOIN lateral ( - SELECT - p.must_be_domain - FROM - projections.login_names3_policies p - WHERE - u.instance_id = p.instance_id - AND ( - (p.is_default IS TRUE AND p.instance_id = $2) - OR (p.instance_id = $2 AND p.resource_owner = u.resource_owner) - ) - ORDER BY is_default - LIMIT 1 - ) p ON TRUE - JOIN - projections.login_names3_domains d - ON - u.instance_id = d.instance_id - AND u.resource_owner = d.resource_owner - WHERE - u.instance_id = $2 - AND u.id = $1 -) SELECT u.id , u.creation_date @@ -45,8 +7,8 @@ SELECT , u.state , u.type , u.username - , (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln GROUP BY ln.user_id, ln.instance_id) login_names - , (SELECT ln.login_name login_names_lower FROM login_names ln WHERE ln.is_primary IS TRUE) preferred_login_name + , login_names.login_names AS login_names + , login_names.preferred_login_name AS preferred_login_name , h.user_id , h.first_name , h.last_name @@ -73,6 +35,16 @@ LEFT JOIN ON u.id = n.user_id AND u.instance_id = n.instance_id +LEFT JOIN LATERAL ( + SELECT + ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, + MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name + FROM + projections.login_names3 AS ln + WHERE + ln.user_id = u.id + AND ln.instance_id = u.instance_id +) AS login_names ON TRUE WHERE u.id = $1 AND u.instance_id = $2 diff --git a/internal/query/user_notify_by_login_name.sql b/internal/query/user_notify_by_login_name.sql index 5b23cd61a7..090a8991f9 100644 --- a/internal/query/user_notify_by_login_name.sql +++ b/internal/query/user_notify_by_login_name.sql @@ -10,7 +10,7 @@ WITH found_users AS ( LEFT JOIN projections.login_names3_policies p_custom ON u.instance_id = p_custom.instance_id AND p_custom.instance_id = $4 AND p_custom.resource_owner = u.resource_owner - LEFT JOIN projections.login_names3_policies p_default + JOIN projections.login_names3_policies p_default ON u.instance_id = p_default.instance_id AND p_default.instance_id = $4 AND p_default.is_default IS TRUE AND ( diff --git a/internal/query/user_personal_access_token.go b/internal/query/user_personal_access_token.go index 8ea33f51a4..49281d9f90 100644 --- a/internal/query/user_personal_access_token.go +++ b/internal/query/user_personal_access_token.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -11,12 +12,21 @@ 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/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) +func patsCheckPermission(ctx context.Context, tokens *PersonalAccessTokens, permissionCheck domain.PermissionCheck) { + tokens.PersonalAccessTokens = slices.DeleteFunc(tokens.PersonalAccessTokens, + func(token *PersonalAccessToken) bool { + return userCheckPermission(ctx, token.ResourceOwner, token.UserID, permissionCheck) != nil + }, + ) +} + var ( personalAccessTokensTable = table{ name: projection.PersonalAccessTokenProjectionTable, @@ -86,7 +96,7 @@ type PersonalAccessTokenSearchQueries struct { Queries []SearchQuery } -func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (pat *PersonalAccessToken, err error) { +func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk bool, id string, queries ...SearchQuery) (pat *PersonalAccessToken, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -102,15 +112,13 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk query = q.toQuery(query) } eq := sq.Eq{ - PersonalAccessTokenColumnID.identifier(): id, - PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), - } - if !withOwnerRemoved { - eq[PersonalAccessTokenColumnOwnerRemoved.identifier()] = false + PersonalAccessTokenColumnID.identifier(): id, + PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + PersonalAccessTokenColumnOwnerRemoved.identifier(): false, } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgfb4", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Dgfb4", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -123,18 +131,34 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk return pat, nil } -func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, withOwnerRemoved bool) (personalAccessTokens *PersonalAccessTokens, err error) { +// SearchPersonalAccessTokens returns personal access token resources. +// If permissionCheck is nil, the PATs are not filtered. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is false, the returned PATs are filtered in-memory by the given permission check. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is true, the returned PATs are filtered in the database. +func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, permissionCheck domain.PermissionCheck) (authNKeys *PersonalAccessTokens, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + keys, err := q.searchPersonalAccessTokens(ctx, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + patsCheckPermission(ctx, keys, permissionCheck) + } + return keys, nil +} + +func (q *Queries) searchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, permissionCheckV2 bool) (personalAccessTokens *PersonalAccessTokens, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := preparePersonalAccessTokensQuery() + query = queries.toQuery(query) + query = userPermissionCheckV2WithCustomColumns(ctx, query, permissionCheckV2, queries.Queries, PersonalAccessTokenColumnResourceOwner, PersonalAccessTokenColumnUserID) eq := sq.Eq{ - PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + PersonalAccessTokenColumnOwnerRemoved.identifier(): false, } - if !withOwnerRemoved { - eq[PersonalAccessTokenColumnOwnerRemoved.identifier()] = false - } - stmt, args, err := queries.toQuery(query).Where(eq).ToSql() + stmt, args, err := query.Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-Hjw2w", "Errors.Query.InvalidRequest") } @@ -160,6 +184,18 @@ func NewPersonalAccessTokenUserIDSearchQuery(value string) (SearchQuery, error) return NewTextQuery(PersonalAccessTokenColumnUserID, value, TextEquals) } +func NewPersonalAccessTokenIDQuery(id string) (SearchQuery, error) { + return NewTextQuery(PersonalAccessTokenColumnID, id, TextEquals) +} + +func NewPersonalAccessTokenCreationDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(PersonalAccessTokenColumnCreationDate, ts, compare) +} + +func NewPersonalAccessTokenExpirationDateDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(PersonalAccessTokenColumnExpiration, ts, compare) +} + func (r *PersonalAccessTokenSearchQueries) AppendMyResourceOwnerQuery(orgID string) error { query, err := NewPersonalAccessTokenResourceOwnerSearchQuery(orgID) if err != nil { diff --git a/internal/query/user_test.go b/internal/query/user_test.go index 50d65cc1ec..ae5f6be207 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -222,87 +222,6 @@ func TestUser_userCheckPermission(t *testing.T) { } var ( - loginNamesQuery = `SELECT login_names.user_id, ARRAY_AGG(login_names.login_name)::TEXT[] AS loginnames, ARRAY_AGG(LOWER(login_names.login_name))::TEXT[] AS loginnames_lower, login_names.instance_id` + - ` FROM projections.login_names3 AS login_names` + - ` GROUP BY login_names.user_id, login_names.instance_id` - preferredLoginNameQuery = `SELECT preferred_login_name.user_id, preferred_login_name.login_name, preferred_login_name.instance_id` + - ` FROM projections.login_names3 AS preferred_login_name` + - ` WHERE preferred_login_name.is_primary = $1` - userQuery = `SELECT projections.users14.id,` + - ` projections.users14.creation_date,` + - ` projections.users14.change_date,` + - ` projections.users14.resource_owner,` + - ` projections.users14.sequence,` + - ` projections.users14.state,` + - ` projections.users14.type,` + - ` projections.users14.username,` + - ` login_names.loginnames,` + - ` preferred_login_name.login_name,` + - ` projections.users14_humans.user_id,` + - ` projections.users14_humans.first_name,` + - ` projections.users14_humans.last_name,` + - ` projections.users14_humans.nick_name,` + - ` projections.users14_humans.display_name,` + - ` projections.users14_humans.preferred_language,` + - ` projections.users14_humans.gender,` + - ` projections.users14_humans.avatar_key,` + - ` projections.users14_humans.email,` + - ` projections.users14_humans.is_email_verified,` + - ` projections.users14_humans.phone,` + - ` projections.users14_humans.is_phone_verified,` + - ` projections.users14_humans.password_change_required,` + - ` projections.users14_humans.password_changed,` + - ` projections.users14_humans.mfa_init_skipped,` + - ` projections.users14_machines.user_id,` + - ` projections.users14_machines.name,` + - ` projections.users14_machines.description,` + - ` projections.users14_machines.secret,` + - ` projections.users14_machines.access_token_type,` + - ` COUNT(*) OVER ()` + - ` 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` + - ` LEFT JOIN projections.users14_machines ON projections.users14.id = projections.users14_machines.user_id AND projections.users14.instance_id = projections.users14_machines.instance_id` + - ` LEFT JOIN` + - ` (` + loginNamesQuery + `) AS login_names` + - ` 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` - userCols = []string{ - "id", - "creation_date", - "change_date", - "resource_owner", - "sequence", - "state", - "type", - "username", - "loginnames", - "login_name", - // human - "user_id", - "first_name", - "last_name", - "nick_name", - "display_name", - "preferred_language", - "gender", - "avatar_key", - "email", - "is_email_verified", - "phone", - "is_phone_verified", - "password_change_required", - "password_changed", - "mfa_init_skipped", - // machine - "user_id", - "name", - "description", - "secret", - "access_token_type", - "count", - } profileQuery = `SELECT projections.users14.id,` + ` projections.users14.creation_date,` + ` projections.users14.change_date,` + @@ -397,8 +316,8 @@ var ( ` projections.users14.state,` + ` projections.users14.type,` + ` projections.users14.username,` + - ` login_names.loginnames,` + - ` preferred_login_name.login_name,` + + ` login_names.login_names,` + + ` login_names.preferred_login_name,` + ` projections.users14_humans.user_id,` + ` projections.users14_humans.first_name,` + ` projections.users14_humans.last_name,` + @@ -417,12 +336,7 @@ var ( ` 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` + ` LEFT JOIN projections.users14_notifications ON projections.users14.id = projections.users14_notifications.user_id AND projections.users14.instance_id = projections.users14_notifications.instance_id` + - ` LEFT JOIN` + - ` (` + loginNamesQuery + `) AS login_names` + - ` 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` + ` LEFT JOIN LATERAL (SELECT ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name FROM projections.login_names3 AS ln WHERE ln.user_id = projections.users14.id AND ln.instance_id = projections.users14.instance_id) AS login_names ON TRUE` notifyUserCols = []string{ "id", "creation_date", @@ -432,8 +346,8 @@ var ( "state", "type", "username", - "loginnames", - "login_name", + "login_names", + "preferred_login_name", // human "user_id", "first_name", @@ -460,8 +374,8 @@ var ( ` projections.users14.state,` + ` projections.users14.type,` + ` projections.users14.username,` + - ` login_names.loginnames,` + - ` preferred_login_name.login_name,` + + ` login_names.login_names,` + + ` login_names.preferred_login_name,` + ` projections.users14_humans.user_id,` + ` projections.users14_humans.first_name,` + ` projections.users14_humans.last_name,` + @@ -485,12 +399,7 @@ var ( ` 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` + ` LEFT JOIN projections.users14_machines ON projections.users14.id = projections.users14_machines.user_id AND projections.users14.instance_id = projections.users14_machines.instance_id` + - ` LEFT JOIN` + - ` (` + loginNamesQuery + `) AS login_names` + - ` 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` + ` LEFT JOIN LATERAL (SELECT ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name FROM projections.login_names3 AS ln WHERE ln.user_id = projections.users14.id AND ln.instance_id = projections.users14.instance_id) AS login_names ON TRUE` usersCols = []string{ "id", "creation_date", @@ -500,8 +409,8 @@ var ( "state", "type", "username", - "loginnames", - "login_name", + "login_names", + "preferred_login_name", // human "user_id", "first_name", @@ -540,240 +449,6 @@ func Test_UserPrepares(t *testing.T) { want want object interface{} }{ - { - name: "prepareUserQuery no result", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQueryScanErr( - regexp.QuoteMeta(userQuery), - 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: (*User)(nil), - }, - { - name: "prepareUserQuery human found", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQuery( - regexp.QuoteMeta(userQuery), - userCols, - []driver.Value{ - "id", - testNow, - testNow, - "resource_owner", - uint64(20211108), - domain.UserStateActive, - domain.UserTypeHuman, - "username", - database.TextArray[string]{"login_name1", "login_name2"}, - "login_name1", - // human - "id", - "first_name", - "last_name", - "nick_name", - "display_name", - "de", - domain.GenderUnspecified, - "avatar_key", - "email", - true, - "phone", - true, - true, - testNow, - testNow, - // machine - nil, - nil, - nil, - nil, - nil, - 1, - }, - ), - }, - object: &User{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - ResourceOwner: "resource_owner", - Sequence: 20211108, - State: domain.UserStateActive, - Type: domain.UserTypeHuman, - Username: "username", - LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, - PreferredLoginName: "login_name1", - Human: &Human{ - FirstName: "first_name", - LastName: "last_name", - NickName: "nick_name", - DisplayName: "display_name", - AvatarKey: "avatar_key", - PreferredLanguage: language.German, - Gender: domain.GenderUnspecified, - Email: "email", - IsEmailVerified: true, - Phone: "phone", - IsPhoneVerified: true, - PasswordChangeRequired: true, - PasswordChanged: testNow, - MFAInitSkipped: testNow, - }, - }, - }, - { - name: "prepareUserQuery machine found", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQuery( - regexp.QuoteMeta(userQuery), - userCols, - []driver.Value{ - "id", - testNow, - testNow, - "resource_owner", - uint64(20211108), - domain.UserStateActive, - domain.UserTypeMachine, - "username", - database.TextArray[string]{"login_name1", "login_name2"}, - "login_name1", - // human - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - // machine - "id", - "name", - "description", - nil, - domain.OIDCTokenTypeBearer, - 1, - }, - ), - }, - object: &User{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - ResourceOwner: "resource_owner", - Sequence: 20211108, - State: domain.UserStateActive, - Type: domain.UserTypeMachine, - Username: "username", - LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, - PreferredLoginName: "login_name1", - Machine: &Machine{ - Name: "name", - Description: "description", - EncodedSecret: "", - AccessTokenType: domain.OIDCTokenTypeBearer, - }, - }, - }, - { - name: "prepareUserQuery machine with secret found", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQuery( - regexp.QuoteMeta(userQuery), - userCols, - []driver.Value{ - "id", - testNow, - testNow, - "resource_owner", - uint64(20211108), - domain.UserStateActive, - domain.UserTypeMachine, - "username", - database.TextArray[string]{"login_name1", "login_name2"}, - "login_name1", - // human - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - // machine - "id", - "name", - "description", - "secret", - domain.OIDCTokenTypeBearer, - 1, - }, - ), - }, - object: &User{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - ResourceOwner: "resource_owner", - Sequence: 20211108, - State: domain.UserStateActive, - Type: domain.UserTypeMachine, - Username: "username", - LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, - PreferredLoginName: "login_name1", - Machine: &Machine{ - Name: "name", - Description: "description", - EncodedSecret: "secret", - AccessTokenType: domain.OIDCTokenTypeBearer, - }, - }, - }, - { - name: "prepareUserQuery sql err", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQueryErr( - regexp.QuoteMeta(userQuery), - 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: (*User)(nil), - }, { name: "prepareProfileQuery no result", prepare: prepareProfileQuery, diff --git a/internal/queue/queue.go b/internal/queue/queue.go index d680221753..22df8c2b5c 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -7,9 +7,12 @@ import ( "github.com/riverqueue/river" "github.com/riverqueue/river/riverdriver" "github.com/riverqueue/river/riverdriver/riverpgxv5" + "github.com/riverqueue/river/rivertype" + "github.com/riverqueue/rivercontrib/otelriver" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/telemetry/metrics" ) // Queue abstracts the underlying queuing library @@ -27,12 +30,16 @@ type Config struct { } func NewQueue(config *Config) (_ *Queue, err error) { + middleware := []rivertype.Middleware{otelriver.NewMiddleware(&otelriver.MiddlewareConfig{ + MeterProvider: metrics.GetMetricsProvider(), + })} return &Queue{ driver: riverpgxv5.New(config.Client.Pool), config: &river.Config{ Workers: river.NewWorkers(), Queues: make(map[string]river.QueueConfig), JobTimeout: -1, + Middleware: middleware, }, }, nil } diff --git a/internal/repository/idp/saml.go b/internal/repository/idp/saml.go index da6a52b1be..209d8371b3 100644 --- a/internal/repository/idp/saml.go +++ b/internal/repository/idp/saml.go @@ -19,6 +19,7 @@ type SAMLIDPAddedEvent struct { WithSignedRequest bool `json:"withSignedRequest,omitempty"` NameIDFormat *domain.SAMLNameIDFormat `json:"nameIDFormat,omitempty"` TransientMappingAttributeName string `json:"transientMappingAttributeName,omitempty"` + FederatedLogoutEnabled bool `json:"federatedLogoutEnabled,omitempty"` Options } @@ -33,6 +34,7 @@ func NewSAMLIDPAddedEvent( withSignedRequest bool, nameIDFormat *domain.SAMLNameIDFormat, transientMappingAttributeName string, + federatedLogoutEnabled bool, options Options, ) *SAMLIDPAddedEvent { return &SAMLIDPAddedEvent{ @@ -46,6 +48,7 @@ func NewSAMLIDPAddedEvent( WithSignedRequest: withSignedRequest, NameIDFormat: nameIDFormat, TransientMappingAttributeName: transientMappingAttributeName, + FederatedLogoutEnabled: federatedLogoutEnabled, Options: options, } } @@ -83,6 +86,7 @@ type SAMLIDPChangedEvent struct { WithSignedRequest *bool `json:"withSignedRequest,omitempty"` NameIDFormat *domain.SAMLNameIDFormat `json:"nameIDFormat,omitempty"` TransientMappingAttributeName *string `json:"transientMappingAttributeName,omitempty"` + FederatedLogoutEnabled *bool `json:"federatedLogoutEnabled,omitempty"` OptionChanges } @@ -154,6 +158,12 @@ func ChangeSAMLTransientMappingAttributeName(name string) func(*SAMLIDPChangedEv } } +func ChangeSAMLFederatedLogoutEnabled(federatedLogoutEnabled bool) func(*SAMLIDPChangedEvent) { + return func(e *SAMLIDPChangedEvent) { + e.FederatedLogoutEnabled = &federatedLogoutEnabled + } +} + func ChangeSAMLOptions(options OptionChanges) func(*SAMLIDPChangedEvent) { return func(e *SAMLIDPChangedEvent) { e.OptionChanges = options diff --git a/internal/repository/idpintent/eventstore.go b/internal/repository/idpintent/eventstore.go index ea94803973..6bec32c735 100644 --- a/internal/repository/idpintent/eventstore.go +++ b/internal/repository/idpintent/eventstore.go @@ -11,4 +11,5 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, SAMLRequestEventType, SAMLRequestEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, LDAPSucceededEventType, LDAPSucceededEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, FailedEventType, FailedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, ConsumedEventType, eventstore.GenericEventMapper[ConsumedEvent]) } diff --git a/internal/repository/idpintent/intent.go b/internal/repository/idpintent/intent.go index 27e6391f95..e4ee28cae9 100644 --- a/internal/repository/idpintent/intent.go +++ b/internal/repository/idpintent/intent.go @@ -3,6 +3,7 @@ package idpintent import ( "context" "net/url" + "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" @@ -16,6 +17,7 @@ const ( SAMLRequestEventType = instanceEventTypePrefix + "saml.requested" LDAPSucceededEventType = instanceEventTypePrefix + "ldap.succeeded" FailedEventType = instanceEventTypePrefix + "failed" + ConsumedEventType = instanceEventTypePrefix + "consumed" ) type StartedEvent struct { @@ -79,6 +81,7 @@ type SucceededEvent struct { IDPAccessToken *crypto.CryptoValue `json:"idpAccessToken,omitempty"` IDPIDToken string `json:"idpIdToken,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` } func NewSucceededEvent( @@ -90,6 +93,7 @@ func NewSucceededEvent( userID string, idpAccessToken *crypto.CryptoValue, idpIDToken string, + expiresAt time.Time, ) *SucceededEvent { return &SucceededEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -103,6 +107,7 @@ func NewSucceededEvent( UserID: userID, IDPAccessToken: idpAccessToken, IDPIDToken: idpIDToken, + ExpiresAt: expiresAt, } } @@ -136,6 +141,7 @@ type SAMLSucceededEvent struct { UserID string `json:"userId,omitempty"` Assertion *crypto.CryptoValue `json:"assertion,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` } func NewSAMLSucceededEvent( @@ -146,6 +152,7 @@ func NewSAMLSucceededEvent( idpUserName, userID string, assertion *crypto.CryptoValue, + expiresAt time.Time, ) *SAMLSucceededEvent { return &SAMLSucceededEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -158,6 +165,7 @@ func NewSAMLSucceededEvent( IDPUserName: idpUserName, UserID: userID, Assertion: assertion, + ExpiresAt: expiresAt, } } @@ -233,6 +241,7 @@ type LDAPSucceededEvent struct { UserID string `json:"userId,omitempty"` EntryAttributes map[string][]string `json:"user,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` } func NewLDAPSucceededEvent( @@ -243,6 +252,7 @@ func NewLDAPSucceededEvent( idpUserName, userID string, attributes map[string][]string, + expiresAt time.Time, ) *LDAPSucceededEvent { return &LDAPSucceededEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -255,6 +265,7 @@ func NewLDAPSucceededEvent( IDPUserName: idpUserName, UserID: userID, EntryAttributes: attributes, + ExpiresAt: expiresAt, } } @@ -320,3 +331,32 @@ func FailedEventMapper(event eventstore.Event) (eventstore.Event, error) { return e, nil } + +type ConsumedEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func NewConsumedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *ConsumedEvent { + return &ConsumedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + ConsumedEventType, + ), + } +} + +func (e *ConsumedEvent) Payload() interface{} { + return e +} + +func (e *ConsumedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *ConsumedEvent) SetBaseEvent(base *eventstore.BaseEvent) { + e.BaseEvent = *base +} diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go index 6ab60c0dd5..f0f324cb7d 100644 --- a/internal/repository/instance/idp.go +++ b/internal/repository/instance/idp.go @@ -1024,6 +1024,7 @@ func NewSAMLIDPAddedEvent( withSignedRequest bool, nameIDFormat *domain.SAMLNameIDFormat, transientMappingAttributeName string, + federatedLogoutEnabled bool, options idp.Options, ) *SAMLIDPAddedEvent { return &SAMLIDPAddedEvent{ @@ -1042,6 +1043,7 @@ func NewSAMLIDPAddedEvent( withSignedRequest, nameIDFormat, transientMappingAttributeName, + federatedLogoutEnabled, options, ), } diff --git a/internal/repository/org/idp.go b/internal/repository/org/idp.go index 0070f71a95..5f061370f1 100644 --- a/internal/repository/org/idp.go +++ b/internal/repository/org/idp.go @@ -1025,6 +1025,7 @@ func NewSAMLIDPAddedEvent( withSignedRequest bool, nameIDFormat *domain.SAMLNameIDFormat, transientMappingAttributeName string, + federatedLogoutEnabled bool, options idp.Options, ) *SAMLIDPAddedEvent { @@ -1044,6 +1045,7 @@ func NewSAMLIDPAddedEvent( withSignedRequest, nameIDFormat, transientMappingAttributeName, + federatedLogoutEnabled, options, ), } diff --git a/internal/repository/project/aggregate.go b/internal/repository/project/aggregate.go index 5391718812..15cb24278d 100644 --- a/internal/repository/project/aggregate.go +++ b/internal/repository/project/aggregate.go @@ -1,6 +1,8 @@ package project import ( + "context" + "github.com/zitadel/zitadel/internal/eventstore" ) @@ -23,3 +25,7 @@ func NewAggregate(id, resourceOwner string) *Aggregate { }, } } + +func AggregateFromWriteModel(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return eventstore.AggregateFromWriteModelCtx(ctx, wm, AggregateType, AggregateVersion) +} diff --git a/internal/repository/project/project.go b/internal/repository/project/project.go index 44f882b3e1..c86fed5272 100644 --- a/internal/repository/project/project.go +++ b/internal/repository/project/project.go @@ -184,10 +184,7 @@ func NewProjectChangeEvent( aggregate *eventstore.Aggregate, oldName string, changes []ProjectChanges, -) (*ProjectChangeEvent, error) { - if len(changes) == 0 { - return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-mV9xc", "Errors.NoChangesFound") - } +) *ProjectChangeEvent { changeEvent := &ProjectChangeEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, @@ -199,7 +196,7 @@ func NewProjectChangeEvent( for _, change := range changes { change(changeEvent) } - return changeEvent, nil + return changeEvent } type ProjectChanges func(event *ProjectChangeEvent) diff --git a/internal/repository/user/machine.go b/internal/repository/user/machine.go index d76290931a..a466f92fe3 100644 --- a/internal/repository/user/machine.go +++ b/internal/repository/user/machine.go @@ -88,10 +88,7 @@ func NewMachineChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, changes []MachineChanges, -) (*MachineChangedEvent, error) { - if len(changes) == 0 { - return nil, zerrors.ThrowPreconditionFailed(nil, "USER-3M9fs", "Errors.NoChangesFound") - } +) *MachineChangedEvent { changeEvent := &MachineChangedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, @@ -102,7 +99,7 @@ func NewMachineChangedEvent( for _, change := range changes { change(changeEvent) } - return changeEvent, nil + return changeEvent } type MachineChanges func(event *MachineChangedEvent) diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index d7dc18898b..d58b2eb64a 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -117,6 +117,7 @@ Errors: AlreadyVerified: Телефонът вече е потвърден Empty: Телефонът е празен NotChanged: Телефонът не е сменен + VerifyingRemovalIsNotSupported: Премахването на проверката не се поддържа Address: NotFound: Адресът не е намерен NotChanged: Адресът не е променен @@ -554,6 +555,7 @@ Errors: StateMissing: В заявката липсва параметър състояние NotStarted: Намерението не е стартирано или вече е прекратено NotSucceeded: Намерението не е успешно + Expired: Намерението е изтекло TokenCreationFailed: Неуспешно създаване на токен InvalidToken: Знакът за намерение е невалиден OtherUser: Намерение, предназначено за друг потребител diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 80db4952f9..d248ce4ca7 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefon již ověřen Empty: Telefon je prázdný NotChanged: Telefon nezměněn + VerifyingRemovalIsNotSupported: Ověření odstranění telefonu není podporováno Address: NotFound: Adresa nenalezena NotChanged: Adresa nezměněna @@ -534,6 +535,7 @@ Errors: StateMissing: V požadavku chybí parametr stavu NotStarted: Záměr nebyl zahájen nebo již byl ukončen NotSucceeded: Záměr nebyl úspěšný + Expired: Záměr vypršel TokenCreationFailed: Vytvoření tokenu selhalo InvalidToken: Token záměru je neplatný OtherUser: Záměr určený pro jiného uživatele diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index dcb3ac5c71..96edf57456 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefonnummer bereits verifiziert Empty: Telefonnummer ist leer NotChanged: Telefonnummer wurde nicht geändert + VerifyingRemovalIsNotSupported: Verifizieren der Telefonnummer Entfernung wird nicht unterstützt Address: NotFound: Adresse nicht gefunden NotChanged: Adresse wurde nicht geändert @@ -536,6 +537,7 @@ Errors: StateMissing: State parameter fehlt im Request NotStarted: Intent wurde nicht gestartet oder wurde bereits beendet NotSucceeded: Intent war nicht erfolgreich + Expired: Intent ist abgelaufen TokenCreationFailed: Tokenerstellung schlug fehl InvalidToken: Intent Token ist ungültig OtherUser: Intent ist für anderen Benutzer gedacht diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index bd8d26d727..0f512defe4 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: Phone already verified Empty: Phone is empty NotChanged: Phone not changed + VerifyingRemovalIsNotSupported: Verifying phone removal is not supported Address: NotFound: Address not found NotChanged: Address not changed @@ -537,6 +538,7 @@ Errors: StateMissing: State parameter is missing in the request NotStarted: Intent is not started or was already terminated NotSucceeded: Intent has not succeeded + Expired: Intent has expired TokenCreationFailed: Token creation failed InvalidToken: Intent Token is invalid OtherUser: Intent meant for another user diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 9f11b63964..8c901f8ebe 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: El teléfono ya se verificó Empty: El teléfono está vacío NotChanged: El teléfono no ha cambiado + VerifyingRemovalIsNotSupported: La verificación de eliminación no está soportada Address: NotFound: Dirección no encontrada NotChanged: La dirección no ha cambiado @@ -536,6 +537,7 @@ Errors: StateMissing: Falta un parámetro de estado en la solicitud NotStarted: La intención no se ha iniciado o ya ha finalizado NotSucceeded: Intento fallido + Expired: La intención ha expirado TokenCreationFailed: Fallo en la creación del token InvalidToken: El token de la intención no es válido OtherUser: Destinado a otro usuario diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index ff8393befc..2a2a51d7c4 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Téléphone déjà vérifié Empty: Téléphone est vide NotChanged: Téléphone n'a pas changé + VerifyingRemovalIsNotSupported: La vérification de la suppression n'est pas prise en charge Address: NotFound: Adresse non trouvée NotChanged: L'adresse n'a pas changé @@ -536,6 +537,7 @@ Errors: StateMissing: Paramètre d'état manquant dans la requête NotStarted: Intent n'a pas démarré ou s'est déjà terminé NotSucceeded: l'intention n'a pas abouti + Expired: L'intention a expiré TokenCreationFailed: La création du token a échoué InvalidToken: Le jeton d'intention n'est pas valide OtherUser: Intention destinée à un autre utilisateur diff --git a/internal/static/i18n/hu.yaml b/internal/static/i18n/hu.yaml index b17c6a1225..a4cc908fa2 100644 --- a/internal/static/i18n/hu.yaml +++ b/internal/static/i18n/hu.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefon már ellenőrizve Empty: A telefon mező üres NotChanged: Telefon nem lett megváltoztatva + VerifyingRemovalIsNotSupported: A telefon eltávolításának ellenőrzése nem támogatott Address: NotFound: Cím nem található NotChanged: Cím nem lett megváltoztatva @@ -536,6 +537,7 @@ Errors: StateMissing: A kérésből hiányzik a State paraméter NotStarted: Az intent nem indult el, vagy már befejeződött NotSucceeded: Az intent nem sikerült + Expired: A kérésből lejárt TokenCreationFailed: A token létrehozása nem sikerült InvalidToken: Az Intent Token érvénytelen OtherUser: Az intent egy másik felhasználónak szól diff --git a/internal/static/i18n/id.yaml b/internal/static/i18n/id.yaml index 56a454e71d..c9187020f7 100644 --- a/internal/static/i18n/id.yaml +++ b/internal/static/i18n/id.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telepon sudah diverifikasi Empty: Telepon kosong NotChanged: Telepon tidak berubah + VerifyingRemovalIsNotSupported: Verifikasi penghapusan tidak didukung Address: NotFound: Alamat tidak ditemukan NotChanged: Alamat tidak berubah @@ -536,6 +537,7 @@ Errors: StateMissing: Parameter status tidak ada dalam permintaan NotStarted: Niat belum dimulai atau sudah dihentikan NotSucceeded: Niatnya belum berhasil + Expired: Kode sudah habis masa berlakunya TokenCreationFailed: Pembuatan token gagal InvalidToken: Token Niat tidak valid OtherUser: Maksudnya ditujukan untuk pengguna lain diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 6713abf2e1..d1dccef4c7 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefono già verificato Empty: Il telefono è vuoto NotChanged: Telefono non cambiato + VerifyingRemovalIsNotSupported: La rimozione della verifica non è supportata Address: NotFound: Indirizzo non trovato NotChanged: Indirizzo non cambiato @@ -536,6 +537,7 @@ Errors: StateMissing: parametro di stato mancante nella richiesta NotStarted: l'intento non è stato avviato o è già stato terminato NotSucceeded: l'intento non è andato a buon fine + Expired: L'intento è scaduto TokenCreationFailed: creazione del token fallita InvalidToken: Il token dell'intento non è valido OtherUser: Intento destinato a un altro utente diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index f57d0f6661..4b0f2ea203 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: 電話番号はすでに認証済みです Empty: 電話番号が空です NotChanged: 電話番号が変更されていません + VerifyingRemovalIsNotSupported: 電話番号の削除を検証することはできません Address: NotFound: 住所が見つかりません NotChanged: 住所は変更されていません @@ -537,6 +538,7 @@ Errors: StateMissing: リクエストに State パラメータがありません NotStarted: インテントが開始されなかったか、既に終了している NotSucceeded: インテントが成功しなかった + Expired: 意図の有効期限が切れました TokenCreationFailed: トークンの作成に失敗しました InvalidToken: インテントのトークンが無効である OtherUser: 他のユーザーを意図している diff --git a/internal/static/i18n/ko.yaml b/internal/static/i18n/ko.yaml index d238142e01..2c87aa1f97 100644 --- a/internal/static/i18n/ko.yaml +++ b/internal/static/i18n/ko.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: 전화번호가 이미 인증되었습니다 Empty: 전화번호가 비어 있습니다 NotChanged: 전화번호가 변경되지 않았습니다 + VerifyingRemovalIsNotSupported: 전화번호 제거를 확인하는 것은 지원되지 않습니다 Address: NotFound: 주소를 찾을 수 없습니다 NotChanged: 주소가 변경되지 않았습니다 @@ -537,6 +538,7 @@ Errors: StateMissing: 요청에 상태 매개변수가 누락되었습니다 NotStarted: 의도가 시작되지 않았거나 이미 종료되었습니다 NotSucceeded: 의도가 성공하지 않았습니다 + Expired: 의도의 유효 기간이 만료되었습니다 TokenCreationFailed: 토큰 생성 실패 InvalidToken: 의도 토큰이 유효하지 않습니다 OtherUser: 다른 사용자를 위한 의도입니다 diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 898ed67360..64ae87a618 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Телефонскиот број веќе е верифициран Empty: Телефонскиот број е празен NotChanged: Телефонскиот број не е променет + VerifyingRemovalIsNotSupported: Отстранувањето на верификацијата не е поддржано Address: NotFound: Адресата не е пронајдена NotChanged: Адресата не е променета @@ -535,6 +536,7 @@ Errors: StateMissing: Параметарот State недостасува во барањето NotStarted: Намерата не е започната или веќе завршена NotSucceeded: Намерата не е успешна + Expired: Намерата е истечена TokenCreationFailed: Неуспешно креирање на токен InvalidToken: Токенот за намера е невалиден OtherUser: Намерата е за друг корисник diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 882c58a4f2..dc9fd83721 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefoon is al geverifieerd Empty: Telefoon is leeg NotChanged: Telefoon niet veranderd + VerifyingRemovalIsNotSupported: Verwijderen van verificatie is niet ondersteund Address: NotFound: Adres niet gevonden NotChanged: Adres niet veranderd @@ -536,6 +537,7 @@ Errors: StateMissing: Staat parameter ontbreekt in het verzoek NotStarted: Intentie is niet gestart of was al beëindigd NotSucceeded: Intentie is niet geslaagd + Expired: Intentie is verlopen TokenCreationFailed: Token aanmaken mislukt InvalidToken: Intentie Token is ongeldig OtherUser: Intentie bedoeld voor een andere gebruiker diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 13125bc2a9..4952345510 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Numer telefonu już zweryfikowany Empty: Numer telefonu jest pusty NotChanged: Numer telefonu nie zmieniony + VerifyingRemovalIsNotSupported: Usunięcie weryfikacji nie jest obsługiwane Address: NotFound: Adres nie znaleziony NotChanged: Adres nie zmieniony @@ -536,6 +537,7 @@ Errors: StateMissing: Brak parametru stanu w żądaniu NotStarted: Intencja nie została rozpoczęta lub już się zakończyła NotSucceeded: intencja nie powiodła się + Expired: Intencja wygasła TokenCreationFailed: Tworzenie tokena nie powiodło się InvalidToken: Token intencji jest nieprawidłowy OtherUser: Intencja przeznaczona dla innego użytkownika diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 4ab3573c2b..e5fc785d0c 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: O telefone já foi verificado Empty: O telefone está vazio NotChanged: Telefone não alterado + VerifyingRemovalIsNotSupported: Remoção de verificação não suportada Address: NotFound: Endereço não encontrado NotChanged: Endereço não alterado @@ -535,6 +536,7 @@ Errors: StateMissing: O parâmetro de estado está faltando na solicitação NotStarted: A intenção não foi iniciada ou já foi encerrada NotSucceeded: A intenção não teve sucesso + Expired: A intenção expirou TokenCreationFailed: Falha na criação do token InvalidToken: O token da intenção é inválido OtherUser: Intenção destinada a outro usuário diff --git a/internal/static/i18n/ro.yaml b/internal/static/i18n/ro.yaml index 48790da9e5..ece4680de6 100644 --- a/internal/static/i18n/ro.yaml +++ b/internal/static/i18n/ro.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: Numărul de telefon este deja verificat Empty: Numărul de telefon este gol NotChanged: Numărul de telefon nu a fost schimbat + VerifyingRemovalIsNotSupported: Verificarea eliminării nu este acceptată Address: NotFound: Adresa nu a fost găsită NotChanged: Adresa nu a fost schimbată @@ -537,6 +538,7 @@ Errors: 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 + Expired: Intenția a expirat TokenCreationFailed: Crearea token-ului a eșuat InvalidToken: Token-ul intenției este invalid OtherUser: Intenția este destinată altui utilizator diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 64a8ef8013..a2efd25322 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: Телефон уже подтверждён Empty: Телефон пуст NotChanged: Телефон не менялся + VerifyingRemovalIsNotSupported: Удаление телефона не поддерживается Address: NotFound: Адрес не найден NotChanged: Адрес не изменён @@ -525,6 +526,7 @@ Errors: StateMissing: В запросе отсутствует параметр State NotStarted: Намерение не начато или уже прекращено NotSucceeded: Намерение не увенчалось успехом + Epired: Намерение истекло TokenCreationFailed: Не удалось создать токен InvalidToken: Маркер намерения недействителен OtherUser: Намерение, предназначенное для другого пользователя diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index 2c292976d3..be40ceba3c 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Mobilnr redan verifierad Empty: Mobilnr är tom NotChanged: Mobilnr ändrades inte + VerifyingRemovalIsNotSupported: Verifiering av borttagning stöds inte Address: NotFound: Adress hittades inte NotChanged: Adress ändrades inte @@ -536,6 +537,7 @@ Errors: StateMissing: State-parameter saknas i begäran NotStarted: Avsikten har inte startat eller har redan avslutats NotSucceeded: Avsikten har inte lyckats + Expired: Avsikten har gått ut TokenCreationFailed: Token-skapande misslyckades InvalidToken: Avsiktstoken är ogiltig OtherUser: Avsikten är avsedd för en annan användare diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index d4b36df7ff..930fcaddae 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: 手机号码已经验证 Empty: 电话号码是空的 NotChanged: 电话号码没有改变 + VerifyingRemovalIsNotSupported: 验证手机号码删除不受支持 Address: NotFound: 找不到地址 NotChanged: 地址没有改变 @@ -536,6 +537,7 @@ Errors: StateMissing: 请求中缺少状态参数 NotStarted: 意图没有开始或已经结束 NotSucceeded: 意图不成功 + Expired: 意图已过期 TokenCreationFailed: 令牌创建失败 InvalidToken: 意图令牌是无效的 OtherUser: 意图是为另一个用户准备的 diff --git a/internal/user/repository/view/user_session.sql b/internal/user/repository/view/user_session.sql new file mode 100644 index 0000000000..3e2a97206b --- /dev/null +++ b/internal/user/repository/view/user_session.sql @@ -0,0 +1,29 @@ +SELECT s.creation_date, + s.change_date, + s.resource_owner, + s.state, + s.user_agent_id, + s.user_id, + u.username, + l.login_name, + h.display_name, + h.avatar_key, + s.selected_idp_config_id, + s.password_verification, + s.passwordless_verification, + s.external_login_verification, + s.second_factor_verification, + s.second_factor_verification_type, + s.multi_factor_verification, + s.multi_factor_verification_type, + s.sequence, + s.instance_id, + s.id +FROM auth.user_sessions s + LEFT JOIN projections.users14 u ON s.user_id = u.id AND s.instance_id = u.instance_id + LEFT JOIN projections.users14_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id + LEFT JOIN projections.login_names3 l ON s.user_id = l.user_id AND s.instance_id = l.instance_id AND l.is_primary = true +WHERE (s.id = $1) + AND (s.instance_id = $2) +LIMIT 1 +; \ No newline at end of file diff --git a/internal/user/repository/view/user_session_view.go b/internal/user/repository/view/user_session_view.go index b3d155f1ec..dec22a181d 100644 --- a/internal/user/repository/view/user_session_view.go +++ b/internal/user/repository/view/user_session_view.go @@ -12,6 +12,9 @@ import ( ) //go:embed user_session_by_id.sql +var userSessionByIDsQuery string + +//go:embed user_session.sql var userSessionByIDQuery string //go:embed user_sessions_by_user_agent.sql @@ -30,7 +33,7 @@ func UserSessionByIDs(ctx context.Context, db *database.DB, agentID, userID, ins userSession, err = scanUserSession(row) return err }, - userSessionByIDQuery, + userSessionByIDsQuery, agentID, userID, instanceID, @@ -38,6 +41,20 @@ func UserSessionByIDs(ctx context.Context, db *database.DB, agentID, userID, ins return userSession, err } +func UserSessionByID(ctx context.Context, db *database.DB, userSessionID, instanceID string) (userSession *model.UserSessionView, err error) { + err = db.QueryRowContext( + ctx, + func(row *sql.Row) error { + userSession, err = scanUserSession(row) + return err + }, + userSessionByIDQuery, + userSessionID, + instanceID, + ) + return userSession, err +} + func UserSessionsByAgentID(ctx context.Context, db *database.DB, agentID, instanceID string) (userSessions []*model.UserSessionView, err error) { err = db.QueryContext( ctx, diff --git a/internal/v2/database/number_filter.go b/internal/v2/database/number_filter.go index ce263ceeee..4853806457 100644 --- a/internal/v2/database/number_filter.go +++ b/internal/v2/database/number_filter.go @@ -3,6 +3,7 @@ package database import ( "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "golang.org/x/exp/constraints" ) @@ -94,7 +95,7 @@ func (c numberCompare) String() string { } type number interface { - constraints.Integer | constraints.Float | time.Time + constraints.Integer | constraints.Float | time.Time | decimal.Decimal // TODO: condition must know if it's args are named parameters or not // constraints.Integer | constraints.Float | time.Time | placeholder } diff --git a/internal/v2/eventstore/event_store.go b/internal/v2/eventstore/event_store.go index cc447c5e15..e89786c657 100644 --- a/internal/v2/eventstore/event_store.go +++ b/internal/v2/eventstore/event_store.go @@ -2,6 +2,8 @@ package eventstore import ( "context" + + "github.com/shopspring/decimal" ) func NewEventstore(querier Querier, pusher Pusher) *EventStore { @@ -30,12 +32,12 @@ type healthier interface { } type GlobalPosition struct { - Position float64 + Position decimal.Decimal InPositionOrder uint32 } func (gp GlobalPosition) IsLess(other GlobalPosition) bool { - return gp.Position < other.Position || (gp.Position == other.Position && gp.InPositionOrder < other.InPositionOrder) + return gp.Position.LessThan(other.Position) || (gp.Position.Equal(other.Position) && gp.InPositionOrder < other.InPositionOrder) } type Reducer interface { diff --git a/internal/v2/eventstore/postgres/push_test.go b/internal/v2/eventstore/postgres/push_test.go index bb3254427c..afd5fe8b8e 100644 --- a/internal/v2/eventstore/postgres/push_test.go +++ b/internal/v2/eventstore/postgres/push_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database/mock" "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/zerrors" @@ -818,7 +820,7 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, }, ), @@ -899,11 +901,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), @@ -984,11 +986,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), @@ -1044,7 +1046,7 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, }, ), @@ -1099,7 +1101,7 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, }, ), @@ -1181,11 +1183,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), @@ -1272,11 +1274,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), diff --git a/internal/v2/eventstore/postgres/query_test.go b/internal/v2/eventstore/postgres/query_test.go index 56f506ac50..34b73bd820 100644 --- a/internal/v2/eventstore/postgres/query_test.go +++ b/internal/v2/eventstore/postgres/query_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database" "github.com/zitadel/zitadel/internal/v2/database/mock" "github.com/zitadel/zitadel/internal/v2/eventstore" @@ -541,13 +543,13 @@ func Test_writeFilter(t *testing.T) { args: args{ filter: eventstore.NewFilter( eventstore.FilterPagination( - eventstore.PositionGreater(123.4, 0), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 0), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND position > $2 ORDER BY position, in_tx_order", - args: []any{"i1", 123.4}, + args: []any{"i1", decimal.NewFromFloat(123.4)}, }, }, { @@ -555,18 +557,18 @@ func Test_writeFilter(t *testing.T) { args: args{ filter: eventstore.NewFilter( eventstore.FilterPagination( - // eventstore.PositionGreater(123.4, 0), + // eventstore.PositionGreater(decimal.NewFromFloat(123.4), 0), // eventstore.PositionLess(125.4, 10), eventstore.PositionBetween( - &eventstore.GlobalPosition{Position: 123.4}, - &eventstore.GlobalPosition{Position: 125.4, InPositionOrder: 10}, + &eventstore.GlobalPosition{Position: decimal.NewFromFloat(123.4)}, + &eventstore.GlobalPosition{Position: decimal.NewFromFloat(125.4), InPositionOrder: 10}, ), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND ((position = $2 AND in_tx_order < $3) OR position < $4) AND position > $5 ORDER BY position, in_tx_order", - args: []any{"i1", 125.4, uint32(10), 125.4, 123.4}, + args: []any{"i1", decimal.NewFromFloat(125.4), uint32(10), decimal.NewFromFloat(125.4), decimal.NewFromFloat(123.4)}, // TODO: (adlerhurst) would require some refactoring to reuse existing args // query: " WHERE instance_id = $1 AND position > $2 AND ((position = $3 AND in_tx_order < $4) OR position < $3) ORDER BY position, in_tx_order", // args: []any{"i1", 123.4, 125.4, uint32(10)}, @@ -577,13 +579,13 @@ func Test_writeFilter(t *testing.T) { args: args{ filter: eventstore.NewFilter( eventstore.FilterPagination( - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND ((position = $2 AND in_tx_order > $3) OR position > $4) ORDER BY position, in_tx_order", - args: []any{"i1", 123.4, uint32(12), 123.4}, + args: []any{"i1", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4)}, }, }, { @@ -593,13 +595,13 @@ func Test_writeFilter(t *testing.T) { eventstore.FilterPagination( eventstore.Limit(10), eventstore.Offset(3), - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND ((position = $2 AND in_tx_order > $3) OR position > $4) ORDER BY position, in_tx_order LIMIT $5 OFFSET $6", - args: []any{"i1", 123.4, uint32(12), 123.4, uint32(10), uint32(3)}, + args: []any{"i1", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4), uint32(10), uint32(3)}, }, }, { @@ -609,14 +611,14 @@ func Test_writeFilter(t *testing.T) { eventstore.FilterPagination( eventstore.Limit(10), eventstore.Offset(3), - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), eventstore.AppendAggregateFilter("user"), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND aggregate_type = $2 AND ((position = $3 AND in_tx_order > $4) OR position > $5) ORDER BY position, in_tx_order LIMIT $6 OFFSET $7", - args: []any{"i1", "user", 123.4, uint32(12), 123.4, uint32(10), uint32(3)}, + args: []any{"i1", "user", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4), uint32(10), uint32(3)}, }, }, { @@ -626,7 +628,7 @@ func Test_writeFilter(t *testing.T) { eventstore.FilterPagination( eventstore.Limit(10), eventstore.Offset(3), - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), eventstore.AppendAggregateFilter("user"), eventstore.AppendAggregateFilter( @@ -637,7 +639,7 @@ func Test_writeFilter(t *testing.T) { }, want: wantQuery{ query: " WHERE instance_id = $1 AND (aggregate_type = $2 OR (aggregate_type = $3 AND aggregate_id = $4)) AND ((position = $5 AND in_tx_order > $6) OR position > $7) ORDER BY position, in_tx_order LIMIT $8 OFFSET $9", - args: []any{"i1", "user", "org", "o1", 123.4, uint32(12), 123.4, uint32(10), uint32(3)}, + args: []any{"i1", "user", "org", "o1", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4), uint32(10), uint32(3)}, }, }, } @@ -956,7 +958,7 @@ func Test_writeQueryUse_examples(t *testing.T) { ), eventstore.FilterPagination( // used because we need to check for first login and an app which is not console - eventstore.PositionGreater(12, 4), + eventstore.PositionGreater(decimal.NewFromInt(12), 4), ), ), eventstore.NewFilter( @@ -1065,9 +1067,9 @@ func Test_writeQueryUse_examples(t *testing.T) { "instance", "user", "user.token.added", - float64(12), + decimal.NewFromInt(12), uint32(4), - float64(12), + decimal.NewFromInt(12), "instance", "instance", []string{"instance.idp.config.added", "instance.idp.oauth.added", "instance.idp.oidc.added", "instance.idp.jwt.added", "instance.idp.azure.added", "instance.idp.github.added", "instance.idp.github.enterprise.added", "instance.idp.gitlab.added", "instance.idp.gitlab.selfhosted.added", "instance.idp.google.added", "instance.idp.ldap.added", "instance.idp.config.apple.added", "instance.idp.saml.added"}, @@ -1201,7 +1203,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), nil, "gigi", @@ -1235,7 +1237,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), []byte(`{"name": "gigi"}`), "gigi", @@ -1269,7 +1271,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), nil, "gigi", @@ -1283,7 +1285,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(24), - float64(124), + decimal.NewFromInt(124).String(), uint32(0), []byte(`{"name": "gigi"}`), "gigi", @@ -1317,7 +1319,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), nil, "gigi", @@ -1331,7 +1333,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(24), - float64(124), + decimal.NewFromInt(124).String(), uint32(0), []byte(`{"name": "gigi"}`), "gigi", diff --git a/internal/v2/eventstore/query.go b/internal/v2/eventstore/query.go index c9b3cecd37..f7a30a2139 100644 --- a/internal/v2/eventstore/query.go +++ b/internal/v2/eventstore/query.go @@ -7,6 +7,8 @@ import ( "slices" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database" ) @@ -723,7 +725,7 @@ func (pc *PositionCondition) Min() *GlobalPosition { // PositionGreater prepares the condition as follows // if inPositionOrder is set: position = AND in_tx_order > OR or position > // if inPositionOrder is NOT set: position > -func PositionGreater(position float64, inPositionOrder uint32) paginationOpt { +func PositionGreater(position decimal.Decimal, inPositionOrder uint32) paginationOpt { return func(p *Pagination) { p.ensurePosition() p.position.min = &GlobalPosition{ @@ -743,7 +745,7 @@ func GlobalPositionGreater(position *GlobalPosition) paginationOpt { // PositionLess prepares the condition as follows // if inPositionOrder is set: position = AND in_tx_order > OR or position > // if inPositionOrder is NOT set: position > -func PositionLess(position float64, inPositionOrder uint32) paginationOpt { +func PositionLess(position decimal.Decimal, inPositionOrder uint32) paginationOpt { return func(p *Pagination) { p.ensurePosition() p.position.max = &GlobalPosition{ diff --git a/internal/v2/eventstore/query_test.go b/internal/v2/eventstore/query_test.go index 00c08914c1..0f313e9560 100644 --- a/internal/v2/eventstore/query_test.go +++ b/internal/v2/eventstore/query_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database" ) @@ -74,13 +76,13 @@ func TestPaginationOpt(t *testing.T) { name: "global position greater", args: args{ opts: []paginationOpt{ - GlobalPositionGreater(&GlobalPosition{Position: 10}), + GlobalPositionGreater(&GlobalPosition{Position: decimal.NewFromInt(10)}), }, }, want: &Pagination{ position: &PositionCondition{ min: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 0, }, }, @@ -90,13 +92,13 @@ func TestPaginationOpt(t *testing.T) { name: "position greater", args: args{ opts: []paginationOpt{ - PositionGreater(10, 0), + PositionGreater(decimal.NewFromInt(10), 0), }, }, want: &Pagination{ position: &PositionCondition{ min: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 0, }, }, @@ -107,13 +109,13 @@ func TestPaginationOpt(t *testing.T) { name: "position less", args: args{ opts: []paginationOpt{ - PositionLess(10, 12), + PositionLess(decimal.NewFromInt(10), 12), }, }, want: &Pagination{ position: &PositionCondition{ max: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 12, }, }, @@ -123,13 +125,13 @@ func TestPaginationOpt(t *testing.T) { name: "global position less", args: args{ opts: []paginationOpt{ - GlobalPositionLess(&GlobalPosition{Position: 12, InPositionOrder: 24}), + GlobalPositionLess(&GlobalPosition{Position: decimal.NewFromInt(12), InPositionOrder: 24}), }, }, want: &Pagination{ position: &PositionCondition{ max: &GlobalPosition{ - Position: 12, + Position: decimal.NewFromInt(12), InPositionOrder: 24, }, }, @@ -140,19 +142,19 @@ func TestPaginationOpt(t *testing.T) { args: args{ opts: []paginationOpt{ PositionBetween( - &GlobalPosition{10, 12}, - &GlobalPosition{20, 0}, + &GlobalPosition{decimal.NewFromInt(10), 12}, + &GlobalPosition{decimal.NewFromInt(20), 0}, ), }, }, want: &Pagination{ position: &PositionCondition{ min: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 12, }, max: &GlobalPosition{ - Position: 20, + Position: decimal.NewFromInt(20), InPositionOrder: 0, }, }, diff --git a/internal/v2/readmodel/last_successful_mirror.go b/internal/v2/readmodel/last_successful_mirror.go index 80b436b896..ca7815b2a8 100644 --- a/internal/v2/readmodel/last_successful_mirror.go +++ b/internal/v2/readmodel/last_successful_mirror.go @@ -1,6 +1,8 @@ package readmodel import ( + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/v2/system" "github.com/zitadel/zitadel/internal/v2/system/mirror" @@ -8,7 +10,7 @@ import ( type LastSuccessfulMirror struct { ID string - Position float64 + Position decimal.Decimal source string } @@ -34,6 +36,7 @@ func (p *LastSuccessfulMirror) Filter() *eventstore.Filter { ), eventstore.FilterPagination( eventstore.Descending(), + eventstore.Limit(1), ), ) } @@ -53,7 +56,7 @@ func (h *LastSuccessfulMirror) Reduce(events ...*eventstore.StorageEvent) (err e func (h *LastSuccessfulMirror) reduceSucceeded(event *eventstore.StorageEvent) error { // if position is set we skip all older events - if h.Position > 0 { + if h.Position.GreaterThan(decimal.NewFromInt(0)) { return nil } diff --git a/internal/v2/readmodel/org.go b/internal/v2/readmodel/org.go index 94bcb21537..ce61ef69b0 100644 --- a/internal/v2/readmodel/org.go +++ b/internal/v2/readmodel/org.go @@ -18,6 +18,7 @@ type Org struct { CreationDate time.Time ChangeDate time.Time Owner string + InstanceID string } func NewOrg(id string) *Org { @@ -60,6 +61,7 @@ func (rm *Org) Reduce(events ...*eventstore.StorageEvent) error { } rm.Sequence = event.Sequence rm.ChangeDate = event.CreatedAt + rm.InstanceID = event.Aggregate.Instance } if err := rm.State.Reduce(events...); err != nil { return err diff --git a/internal/v2/system/mirror/succeeded.go b/internal/v2/system/mirror/succeeded.go index 6d0fba2c25..34d74f184f 100644 --- a/internal/v2/system/mirror/succeeded.go +++ b/internal/v2/system/mirror/succeeded.go @@ -1,6 +1,8 @@ package mirror import ( + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -9,7 +11,7 @@ type succeededPayload struct { // Source is the name of the database data are mirrored from Source string `json:"source"` // Position until data will be mirrored - Position float64 `json:"position"` + Position decimal.Decimal `json:"position"` } const SucceededType = eventTypePrefix + "succeeded" @@ -38,7 +40,7 @@ func SucceededEventFromStorage(event *eventstore.StorageEvent) (e *SucceededEven }, nil } -func NewSucceededCommand(source string, position float64) *eventstore.Command { +func NewSucceededCommand(source string, position decimal.Decimal) *eventstore.Command { return &eventstore.Command{ Action: eventstore.Action[any]{ Creator: Creator, diff --git a/load-test/src/org.ts b/load-test/src/org.ts index f5655432a5..1ed6778d9c 100644 --- a/load-test/src/org.ts +++ b/load-test/src/org.ts @@ -13,7 +13,7 @@ export function createOrg(accessToken: string): Promise { return new Promise((resolve, reject) => { let response = http.asyncRequest( 'POST', - url('/v2beta/organizations'), + url('/v2/organizations'), JSON.stringify({ name: `load-test-${new Date(Date.now()).toISOString()}`, }), diff --git a/load-test/src/use_cases/manipulate_user.ts b/load-test/src/use_cases/manipulate_user.ts index 2ea53bd324..104d81678e 100644 --- a/load-test/src/use_cases/manipulate_user.ts +++ b/load-test/src/use_cases/manipulate_user.ts @@ -15,7 +15,7 @@ export async function setup() { } export default async function (data: any) { - const human = await createHuman(`vu-${__VU}`, data.org, data.tokens.accessToken); + const human = await createHuman(`vu-${__VU}-${new Date(Date.now()).getTime()}`, data.org, data.tokens.accessToken); const updateRes = await updateHuman( { profile: { diff --git a/load-test/src/user.ts b/load-test/src/user.ts index 86ce71fd9b..83a6bba839 100644 --- a/load-test/src/user.ts +++ b/load-test/src/user.ts @@ -30,7 +30,7 @@ export function createHuman(username: string, org: Org, accessToken: string): Pr familyName: 'Zitizen', }, email: { - email: `zitizen-@caos.ch`, + email: `${username}@zitadel.com`, isVerified: true, }, password: { @@ -50,11 +50,11 @@ export function createHuman(username: string, org: Org, accessToken: string): Pr response .then((res) => { check(res, { - 'create user is status ok': (r) => r.status === 201, + 'create user is status ok': (r) => r.status === 200, }) || reject(`unable to create user(username: ${username}) status: ${res.status} body: ${res.body}`); createHumanTrend.add(res.timings.duration); - const user = http.get(url(`/v2beta/users/${res.json('userId')!}`), { + const user = http.get(url(`/v2/users/${res.json('userId')!}`), { headers: { authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', diff --git a/pkg/grpc/user/user.go b/pkg/grpc/user/user.go index 450370e704..a86c957fd8 100644 --- a/pkg/grpc/user/user.go +++ b/pkg/grpc/user/user.go @@ -1,5 +1,7 @@ package user +import "github.com/zitadel/zitadel/internal/domain" + type SearchQuery_ResourceOwner struct { ResourceOwner *ResourceOwnerQuery } @@ -13,3 +15,37 @@ type ResourceOwnerQuery struct { type UserType = isUser_Type type MembershipType = isMembership_Type + +func (s UserState) ToDomain() domain.UserState { + switch s { + case UserState_USER_STATE_UNSPECIFIED: + return domain.UserStateUnspecified + case UserState_USER_STATE_ACTIVE: + return domain.UserStateActive + case UserState_USER_STATE_INACTIVE: + return domain.UserStateInactive + case UserState_USER_STATE_DELETED: + return domain.UserStateDeleted + case UserState_USER_STATE_LOCKED: + return domain.UserStateLocked + case UserState_USER_STATE_SUSPEND: + return domain.UserStateSuspend + case UserState_USER_STATE_INITIAL: + return domain.UserStateInitial + default: + return domain.UserStateUnspecified + } +} + +func (t Type) ToDomain() domain.UserType { + switch t { + case Type_TYPE_UNSPECIFIED: + return domain.UserTypeUnspecified + case Type_TYPE_HUMAN: + return domain.UserTypeHuman + case Type_TYPE_MACHINE: + return domain.UserTypeMachine + default: + return domain.UserTypeUnspecified + } +} diff --git a/pkg/grpc/user/v2/user.go b/pkg/grpc/user/v2/user.go index ec9245c8eb..20c3c6fe9b 100644 --- a/pkg/grpc/user/v2/user.go +++ b/pkg/grpc/user/v2/user.go @@ -1,3 +1,37 @@ package user +import "github.com/zitadel/zitadel/internal/domain" + type UserType = isUser_Type + +func (s UserState) ToDomain() domain.UserState { + switch s { + case UserState_USER_STATE_UNSPECIFIED: + return domain.UserStateUnspecified + case UserState_USER_STATE_ACTIVE: + return domain.UserStateActive + case UserState_USER_STATE_INACTIVE: + return domain.UserStateInactive + case UserState_USER_STATE_DELETED: + return domain.UserStateDeleted + case UserState_USER_STATE_LOCKED: + return domain.UserStateLocked + case UserState_USER_STATE_INITIAL: + return domain.UserStateInitial + default: + return domain.UserStateUnspecified + } +} + +func (t Type) ToDomain() domain.UserType { + switch t { + case Type_TYPE_UNSPECIFIED: + return domain.UserTypeUnspecified + case Type_TYPE_HUMAN: + return domain.UserTypeHuman + case Type_TYPE_MACHINE: + return domain.UserTypeMachine + default: + return domain.UserTypeUnspecified + } +} diff --git a/pkg/grpc/user/v2beta/user.go b/pkg/grpc/user/v2beta/user.go index ec9245c8eb..20c3c6fe9b 100644 --- a/pkg/grpc/user/v2beta/user.go +++ b/pkg/grpc/user/v2beta/user.go @@ -1,3 +1,37 @@ package user +import "github.com/zitadel/zitadel/internal/domain" + type UserType = isUser_Type + +func (s UserState) ToDomain() domain.UserState { + switch s { + case UserState_USER_STATE_UNSPECIFIED: + return domain.UserStateUnspecified + case UserState_USER_STATE_ACTIVE: + return domain.UserStateActive + case UserState_USER_STATE_INACTIVE: + return domain.UserStateInactive + case UserState_USER_STATE_DELETED: + return domain.UserStateDeleted + case UserState_USER_STATE_LOCKED: + return domain.UserStateLocked + case UserState_USER_STATE_INITIAL: + return domain.UserStateInitial + default: + return domain.UserStateUnspecified + } +} + +func (t Type) ToDomain() domain.UserType { + switch t { + case Type_TYPE_UNSPECIFIED: + return domain.UserTypeUnspecified + case Type_TYPE_HUMAN: + return domain.UserTypeHuman + case Type_TYPE_MACHINE: + return domain.UserTypeMachine + default: + return domain.UserTypeUnspecified + } +} diff --git a/proto/buf.yaml b/proto/buf.yaml index 31bc7b4ccc..abe35b3055 100644 --- a/proto/buf.yaml +++ b/proto/buf.yaml @@ -40,4 +40,4 @@ lint: - zitadel/system.proto - zitadel/text.proto - zitadel/user.proto - - zitadel/v1.proto \ No newline at end of file + - zitadel/v1.proto diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index d9f8bee2c7..da496b7c7d 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -1237,6 +1237,7 @@ service AdminService { }; } + // Deprecated: use ListOrganization [apis/resources/org_service_v2beta/organization-service-list-organizations.api.mdx] API instead rpc ListOrgs(ListOrgsRequest) returns (ListOrgsResponse) { option (google.api.http) = { post: "/orgs/_search"; @@ -1256,7 +1257,8 @@ service AdminService { value: { description: "list of organizations matching the query"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { @@ -1271,6 +1273,7 @@ service AdminService { }; } + // Deprecated: use CreateOrganization [apis/resources/org_service_v2beta/organization-service-create-organization.api.mdx] API instead rpc SetUpOrg(SetUpOrgRequest) returns (SetUpOrgResponse) { option (google.api.http) = { post: "/orgs/_setup"; @@ -1290,7 +1293,8 @@ service AdminService { value: { description: "org, user and user membership were created successfully"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { @@ -1305,6 +1309,7 @@ service AdminService { }; } + // Deprecated: use DeleteOrganization [apis/resources/org_service_v2beta/organization-service-delete-organization.api.mdx] API instead rpc RemoveOrg(RemoveOrgRequest) returns (RemoveOrgResponse) { option (google.api.http) = { delete: "/orgs/{org_id}" @@ -1322,7 +1327,8 @@ service AdminService { value: { description: "org removed successfully"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { @@ -7027,6 +7033,9 @@ message AddSAMLProviderRequest { // Optionally specify the 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`. optional string transient_mapping_attribute_name = 8; + // Optionally enable federated logout. If enabled, ZITADEL will send a logout request to the identity provider, + // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata. + optional bool federated_logout_enabled = 9; } message AddSAMLProviderResponse { @@ -7061,6 +7070,9 @@ message UpdateSAMLProviderRequest { // Optionally specify the 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`. optional string transient_mapping_attribute_name = 9; + // Optionally enable federated logout. If enabled, ZITADEL will send a logout request to the identity provider, + // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata. + optional bool federated_logout_enabled = 10; } message UpdateSAMLProviderResponse { @@ -8993,6 +9005,7 @@ message DataOrg { repeated zitadel.management.v1.SetCustomVerifySMSOTPMessageTextRequest verify_sms_otp_messages = 37; repeated zitadel.management.v1.SetCustomVerifyEmailOTPMessageTextRequest verify_email_otp_messages = 38; repeated zitadel.management.v1.SetCustomInviteUserMessageTextRequest invite_user_messages = 39; + zitadel.org.v1.OrgState org_state = 40; } message ImportDataResponse{ diff --git a/proto/zitadel/analytics/v2beta/telemetry.proto b/proto/zitadel/analytics/v2beta/telemetry.proto new file mode 100644 index 0000000000..f0e1537f9a --- /dev/null +++ b/proto/zitadel/analytics/v2beta/telemetry.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package zitadel.analytics.v2beta; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta;analytics"; + + +message InstanceInformation { + // The unique identifier of the instance. + string id = 1; + // The custom domains (incl. generated ones) of the instance. + repeated string domains = 2; + // The creation date of the instance. + google.protobuf.Timestamp created_at = 3; +} + +message ResourceCount { + // The ID of the instance for which the resource counts are reported. + string instance_id = 3; + // The parent type of the resource counts (e.g. organization or instance). + // For example, reporting the amount of users per organization would use + // `COUNT_PARENT_TYPE_ORGANIZATION` as parent type and the organization ID as parent ID. + CountParentType parent_type = 4; + // The parent ID of the resource counts (e.g. organization or instance ID). + // For example, reporting the amount of users per organization would use + // `COUNT_PARENT_TYPE_ORGANIZATION` as parent type and the organization ID as parent ID. + string parent_id = 5; + // The resource counts to report, e.g. amount of `users`, `organizations`, etc. + string resource_name = 6; + // The name of the table in the database, which was used to calculate the counts. + // This can be used to deduplicate counts in case of multiple reports. + // For example, if the counts were calculated from the `users14` table, + // the table name would be `users14`, where there could also be a `users15` table + // reported at the same time as the system is rolling out a new version. + string table_name = 7; + // The timestamp when the count was last updated. + google.protobuf.Timestamp updated_at = 8; + // The actual amount of the resource. + uint32 amount = 9; +} + +enum CountParentType { + COUNT_PARENT_TYPE_UNSPECIFIED = 0; + COUNT_PARENT_TYPE_INSTANCE = 1; + COUNT_PARENT_TYPE_ORGANIZATION = 2; +} diff --git a/proto/zitadel/analytics/v2beta/telemetry_service.proto b/proto/zitadel/analytics/v2beta/telemetry_service.proto new file mode 100644 index 0000000000..e71536a811 --- /dev/null +++ b/proto/zitadel/analytics/v2beta/telemetry_service.proto @@ -0,0 +1,79 @@ +syntax = "proto3"; + +package zitadel.analytics.v2beta; + +import "google/protobuf/timestamp.proto"; +import "zitadel/analytics/v2beta/telemetry.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta;analytics"; + +// The TelemetryService is used to report telemetry such as usage statistics of the ZITADEL instance(s). +// back to a central storage. +// It is used to collect anonymized data about the usage of ZITADEL features, capabilities, and configurations. +// ZITADEL acts as a client of the TelemetryService. +// +// Reports are sent periodically based on the system's runtime configuration. +// The content of the reports, respectively the data collected, can be configured in the system's runtime configuration. +// +// All endpoints follow the same error and retry handling: +// In case of a failure to report the usage, ZITADEL will retry to report the usage +// based on the configured retry policy and error type: +// - Client side errors will not be retried, as they indicate a misconfiguration or an invalid request: +// - `INVALID_ARGUMENT`: The request was malformed. +// - `NOT_FOUND`: The TelemetryService's endpoint is likely misconfigured. +// - Connection / transfer errors will be retried based on the retry policy configured in the system's runtime configuration: +// - `DEADLINE_EXCEEDED`: The request took too long to complete, it will be retried. +// - `RESOURCE_EXHAUSTED`: The request was rejected due to resource exhaustion, it will be retried after a backoff period. +// - `UNAVAILABLE`: The TelemetryService is currently unavailable, it will be retried after a backoff period. +// Server side errors will also be retried based on the information provided by the server: +// - `FAILED_PRECONDITION`: The request failed due to a precondition, e.g. the report ID does not exists, +// does not correspond to the same system ID or previous reporting is too old, do not retry. +// - `INTERNAL`: An internal error occurred. Check details and logs. +service TelemetryService { + + // ReportBaseInformation is used to report the base information of the ZITADEL system, + // including the version, instances, their creation date and domains. + // The response contains a report ID to link it to the resource counts or other reports. + // The report ID is only valid for the same system ID. + rpc ReportBaseInformation (ReportBaseInformationRequest) returns (ReportBaseInformationResponse) {} + + // ReportResourceCounts is used to report the resource counts such as amount of organizations + // or users per organization and much more. + // Since the resource counts can be reported in multiple batches, + // the response contains a report ID to continue reporting. + // The report ID is only valid for the same system ID. + rpc ReportResourceCounts (ReportResourceCountsRequest) returns (ReportResourceCountsResponse) {} +} + +message ReportBaseInformationRequest { + // The system ID is a unique identifier for the ZITADEL system. + string system_id = 1; + // The current version of the ZITADEL system. + string version = 2; + // A list of instances in the ZITADEL system and their information. + repeated InstanceInformation instances = 3; +} + +message ReportBaseInformationResponse { + // The report ID is a unique identifier for the report. + // It is used to identify the report to be able to link it to the resource counts or other reports. + // Note that the report ID is only valid for the same system ID. + string report_id = 1; +} + +message ReportResourceCountsRequest { + // The system ID is a unique identifier for the ZITADEL system. + string system_id = 1; + // The previously returned report ID from the server to continue reporting. + // Note that the report ID is only valid for the same system ID. + optional string report_id = 2; + // A list of resource counts to report. + repeated ResourceCount resource_counts = 3; +} + +message ReportResourceCountsResponse { + // The report ID is a unique identifier for the report. + // It is used to identify the report in case of additional data / pagination. + // Note that the report ID is only valid for the same system ID. + string report_id = 1; +} \ No newline at end of file diff --git a/proto/zitadel/filter/v2/filter.proto b/proto/zitadel/filter/v2/filter.proto new file mode 100644 index 0000000000..3817324d31 --- /dev/null +++ b/proto/zitadel/filter/v2/filter.proto @@ -0,0 +1,96 @@ +syntax = "proto3"; + +package zitadel.filter.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/filter/v2;filter"; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.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_AFTER = 1; + TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS = 2; + TIMESTAMP_FILTER_METHOD_BEFORE = 3; + TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS = 4; +} + +message PaginationRequest { + // Starting point for retrieval, in combination of offset used to query a set list of objects. + uint64 offset = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "0"; + } + ]; + // 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 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "10"; + } + ]; + // 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 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "false"; + } + ]; +} + +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"; + } + ]; +} + +message IDFilter { + // Only return resources that belong to this id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"69629023906488337\""; + } + ]; +} + +message TimestampFilter { + // Filter resources by timestamp. + google.protobuf.Timestamp timestamp = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // Defines the condition (e.g., equals, before, after) that the timestamp of the retrieved resources should match. + TimestampFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} diff --git a/proto/zitadel/filter/v2beta/filter.proto b/proto/zitadel/filter/v2beta/filter.proto index 6aae583cde..2265fa4125 100644 --- a/proto/zitadel/filter/v2beta/filter.proto +++ b/proto/zitadel/filter/v2beta/filter.proto @@ -6,6 +6,7 @@ option go_package = "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta;filter"; import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; enum TextFilterMethod { TEXT_FILTER_METHOD_EQUALS = 0; @@ -56,4 +57,37 @@ message PaginationResponse { example: "\"100\""; } ]; -} \ No newline at end of file +} + +message IDFilter { + // Only return resources that belong to this id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"69629023906488337\""; + } + ]; +} + +message TimestampFilter { + // Filter resources by timestamp. + google.protobuf.Timestamp timestamp = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // Defines the condition (e.g., equals, before, after) that the timestamp of the retrieved resources should match. + TimestampFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message InIDsFilter { + // Defines the ids to query for. + repeated string ids = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"69629023906488334\",\"69622366012355662\"]"; + } + ]; +} diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto index 82e32aa873..de497d8c93 100644 --- a/proto/zitadel/idp.proto +++ b/proto/zitadel/idp.proto @@ -479,6 +479,9 @@ message SAMLConfig { // 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`. optional string transient_mapping_attribute_name = 5; + // Boolean weather federated logout is enabled. If enabled, ZITADEL will send a logout request to the identity provider, + // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata. + optional bool federated_logout_enabled = 6; } message AzureADConfig { diff --git a/proto/zitadel/idp/v2/idp.proto b/proto/zitadel/idp/v2/idp.proto index 0c95b742f1..663581a659 100644 --- a/proto/zitadel/idp/v2/idp.proto +++ b/proto/zitadel/idp/v2/idp.proto @@ -303,6 +303,9 @@ message SAMLConfig { // in case the nameid-format returned is // `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`. optional string transient_mapping_attribute_name = 5; + // Boolean weather federated logout is enabled. If enabled, ZITADEL will send a logout request to the identity provider, + // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata. + optional bool federated_logout_enabled = 6; } message AzureADConfig { diff --git a/proto/zitadel/instance/v2beta/instance.proto b/proto/zitadel/instance/v2beta/instance.proto new file mode 100644 index 0000000000..21f6148490 --- /dev/null +++ b/proto/zitadel/instance/v2beta/instance.proto @@ -0,0 +1,192 @@ +syntax = "proto3"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/object/v2/object.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +package zitadel.instance.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta;instance"; + +message Instance { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + + // 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 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + google.protobuf.Timestamp creation_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + State state = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the instance"; + } + ]; + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + string version = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"1.0.0\""; + } + ]; + repeated Domain domains = 7; +} + +enum State { + STATE_UNSPECIFIED = 0; + STATE_CREATING = 1; + STATE_RUNNING = 2; + STATE_STOPPING = 3; + STATE_STOPPED = 4; +} + +message Domain { + string instance_id = 1; + + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + string domain = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\"" + } + ]; + bool primary = 4; + bool generated = 5; +} + +enum FieldName { + FIELD_NAME_UNSPECIFIED = 0; + FIELD_NAME_ID = 1; + FIELD_NAME_NAME = 2; + FIELD_NAME_CREATION_DATE = 3; +} + +message Query { + oneof query { + option (validate.required) = true; + + IdsQuery id_query = 1; + DomainsQuery domain_query = 2; + } +} + +message IdsQuery { + repeated string ids = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Instance ID"; + example: "[\"4820840938402429\",\"4820840938402422\"]" + } + ]; +} + +message DomainsQuery { + repeated string domains = 1 [ + (validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_items: 20; + example: "[\"my-instace.zitadel.cloud\", \"auth.custom.com\"]"; + description: "Return the instances that have the requested domains"; + } + ]; +} +message DomainSearchQuery { + oneof query { + option (validate.required) = true; + + DomainQuery domain_query = 1; + DomainGeneratedQuery generated_query = 2; + DomainPrimaryQuery primary_query = 3; + } +} + +message DomainQuery { + string domain = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"zitadel.com\""; + } + ]; + zitadel.object.v2.TextQueryMethod 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 DomainGeneratedQuery { + bool generated = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Generated domains"; + } + ]; +} + +message DomainPrimaryQuery { + bool primary = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Primary domains"; + } + ]; +} + +enum DomainFieldName { + DOMAIN_FIELD_NAME_UNSPECIFIED = 0; + DOMAIN_FIELD_NAME_DOMAIN = 1; + DOMAIN_FIELD_NAME_PRIMARY = 2; + DOMAIN_FIELD_NAME_GENERATED = 3; + DOMAIN_FIELD_NAME_CREATION_DATE = 4; +} + +message TrustedDomain { + string instance_id = 1; + + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + string domain = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\"" + } + ]; +} + +message TrustedDomainSearchQuery { + oneof query { + option (validate.required) = true; + + DomainQuery domain_query = 1; + } +} + +enum TrustedDomainFieldName { + TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED = 0; + TRUSTED_DOMAIN_FIELD_NAME_DOMAIN = 1; + TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE = 2; +} diff --git a/proto/zitadel/instance/v2beta/instance_service.proto b/proto/zitadel/instance/v2beta/instance_service.proto new file mode 100644 index 0000000000..0a5de00286 --- /dev/null +++ b/proto/zitadel/instance/v2beta/instance_service.proto @@ -0,0 +1,648 @@ +syntax = "proto3"; + +package zitadel.instance.v2beta; + +import "validate/validate.proto"; +import "zitadel/object/v2/object.proto"; +import "zitadel/instance/v2beta/instance.proto"; +import "zitadel/filter/v2beta/filter.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/empty.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta;instance"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Instance Service"; + version: "2.0-beta"; + description: "This API is intended to manage instances in ZITADEL."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "AGPL-3.0-only", + 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 instances and their domains. +// The service provides methods to create, update, delete and list instances and their domains. +service InstanceService { + + // Delete Instance + // + // Deletes an instance with the given ID. + // + // Required permissions: + // - `system.instance.delete` + rpc DeleteInstance(DeleteInstanceRequest) returns (DeleteInstanceResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The deleted instance."; + } + }; + }; + + option (google.api.http) = { + delete: "/v2beta/instances/{instance_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.instance.delete" + } + }; + } + + // Get Instance + // + // Returns the instance in the current context. + // + // The instace_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.read` + rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The instance of the context."; + } + }; + }; + + option (google.api.http) = { + get: "/v2beta/instances/{instance_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read" + } + }; + } + + // Update Instance + // + // Updates instance in context with the given name. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.write` + rpc UpdateInstance(UpdateInstanceRequest) returns (UpdateInstanceResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The instance was successfully updated."; + } + }; + }; + + option (google.api.http) = { + put: "/v2beta/instances/{instance_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.write" + } + }; + } + + // List Instances + // + // Lists instances matching the given query. + // The query can be used to filter either by instance ID or domain. + // The request is paginated and returns 100 results by default. + // + // Required permissions: + // - `system.instance.read` + rpc ListInstances(ListInstancesRequest) returns (ListInstancesResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The list of instances."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.instance.read" + } + }; + } + + // Add Custom Domain + // + // Adds a custom domain to the instance in context. + // + // The instance_id in the input message will be used in the future + // + // Required permissions: + // - `system.domain.write` + rpc AddCustomDomain(AddCustomDomainRequest) returns (AddCustomDomainResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The added custom domain."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/{instance_id}/custom-domains" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.domain.write" + } + }; + } + + // Remove Custom Domain + // + // Removes a custom domain from the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `system.domain.write` + rpc RemoveCustomDomain(RemoveCustomDomainRequest) returns (RemoveCustomDomainResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The removed custom domain."; + } + }; + }; + + option (google.api.http) = { + delete: "/v2beta/instances/{instance_id}/custom-domains/{domain}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.domain.write" + } + }; + } + + // List Custom Domains + // + // Lists custom domains of the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.read` + rpc ListCustomDomains(ListCustomDomainsRequest) returns (ListCustomDomainsResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The list of custom domains."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/{instance_id}/custom-domains/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read" + } + }; + } + + // Add Trusted Domain + // + // Adds a trusted domain to the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.write` + rpc AddTrustedDomain(AddTrustedDomainRequest) returns (AddTrustedDomainResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The added trusted domain."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/{instance_id}/trusted-domains" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.write" + } + }; + } + + // Remove Trusted Domain + // + // Removes a trusted domain from the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.write` + rpc RemoveTrustedDomain(RemoveTrustedDomainRequest) returns (RemoveTrustedDomainResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The removed trusted domain."; + } + }; + }; + + option (google.api.http) = { + delete: "/v2beta/instances/{instance_id}/trusted-domains/{domain}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.write" + } + }; + } + + + // List Trusted Domains + // + // Lists trusted domains of the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.read` + rpc ListTrustedDomains(ListTrustedDomainsRequest) returns (ListTrustedDomainsResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The list of trusted domains."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/{instance_id}/trusted-domains/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read" + } + }; + } +} + +message DeleteInstanceRequest { + string instance_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: "\"222430354126975533\""; + } + ]; +} + +message DeleteInstanceResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GetInstanceRequest { + string instance_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: "\"222430354126975533\""; + } + ]; +} + +message GetInstanceResponse { + zitadel.instance.v2beta.Instance instance = 1; +} + +message UpdateInstanceRequest { + // used only to identify the instance to change. + string instance_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: "\"222430354126975533\""; + } + ]; + string instance_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + description: "\"name of the instance to update\""; + example: "\"my instance\""; + } + ]; +} + +message UpdateInstanceResponse { + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListInstancesRequest { + // Criterias the client is looking for. + repeated Query queries = 1; + + // Pagination and sorting. + zitadel.filter.v2beta.PaginationRequest pagination = 2; + + // The field the result is sorted by. + optional FieldName sorting_column = 3; +} + +message ListInstancesResponse { + // The list of instances. + repeated Instance instances = 1; + + // Contains the total number of instances matching the query and the applied limit. + zitadel.filter.v2beta.PaginationResponse pagination = 2; +} + +message AddCustomDomainRequest { + string instance_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: "\"222430354126975533\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 253; + } + ]; +} + +message AddCustomDomainResponse { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message RemoveCustomDomainRequest { + string instance_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: "\"222430354126975533\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 253; + } + ]; +} + +message RemoveCustomDomainResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListCustomDomainsRequest { + string instance_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: "\"222430354126975533\""; + } + ]; + + // Pagination and sorting. + zitadel.filter.v2beta.PaginationRequest pagination = 2; + + // The field the result is sorted by. + DomainFieldName sorting_column = 3; + + // Criterias the client is looking for. + repeated DomainSearchQuery queries = 4; +} + +message ListCustomDomainsResponse { + repeated Domain domains = 1; + + // Contains the total number of domains matching the query and the applied limit. + zitadel.filter.v2beta.PaginationResponse pagination = 2; +} + +message AddTrustedDomainRequest { + string instance_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: "\"222430354126975533\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"login.example.com\""; + min_length: 1; + max_length: 253; + } + ]; +} + +message AddTrustedDomainResponse { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message RemoveTrustedDomainRequest { + string instance_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: "\"222430354126975533\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"login.example.com\""; + min_length: 1; + max_length: 253; + } + ]; +} + +message RemoveTrustedDomainResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListTrustedDomainsRequest { + string instance_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: "\"222430354126975533\""; + } + ]; + + // Pagination and sorting. + zitadel.filter.v2beta.PaginationRequest pagination = 2; + + // The field the result is sorted by. + TrustedDomainFieldName sorting_column = 3; + + // Criterias the client is looking for. + repeated TrustedDomainSearchQuery queries = 4; +} + +message ListTrustedDomainsResponse { + repeated TrustedDomain trusted_domain = 1; + + // Contains the total number of domains matching the query and the applied limit. + zitadel.filter.v2beta.PaginationResponse pagination = 2; +} diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index e69e331f87..8cd0b22759 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -432,7 +432,11 @@ service ManagementService { }; } - // Deprecated: use ImportHumanUser + // Create User (Human) + // + // Deprecated: use [ImportHumanUser](apis/resources/mgmt/management-service-import-human-user.api.mdx) instead. + // + // Create a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login. rpc AddHumanUser(AddHumanUserRequest) returns (AddHumanUserResponse) { option (google.api.http) = { post: "/users/human" @@ -444,10 +448,8 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Deprecated: Create User (Human)"; - description: "Create a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.\n\nDeprecated: use ImportHumanUser" - tags: "Users"; deprecated: true; + tags: "Users"; parameters: { headers: { name: "x-zitadel-orgid"; @@ -459,7 +461,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 AddHumanUser + // Create/Import User (Human) + // + // Deprecated: use [UpdateHumanUser](apis/resources/user_service_v2/user-service-update-human-user.api.mdx) instead. + // + // Create/import a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login. rpc ImportHumanUser(ImportHumanUserRequest) returns (ImportHumanUserResponse) { option (google.api.http) = { post: "/users/human/_import" @@ -471,11 +477,9 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create/Import User (Human)"; - description: "Create/import a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.\n\nDeprecated: please use user service v2 [AddHumanUser](apis/resources/user_service_v2/user-service-add-human-user.api.mdx)" + deprecated: true; tags: "Users"; tags: "User Human" - deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -487,6 +491,11 @@ service ManagementService { }; } + // Create User (Machine) + // + // Deprecated: use [user service v2 CreateUser](apis/resources/user_service_v2/user-service-create-user.api.mdx) to create a user of type machine instead. + // + // Create a new user with the type machine for your API, service or device. These users are used for non-interactive authentication flows. rpc AddMachineUser(AddMachineUserRequest) returns (AddMachineUserResponse) { option (google.api.http) = { post: "/users/machine" @@ -498,8 +507,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create User (Machine)"; - description: "Create a new user with the type machine for your API, service or device. These users are used for non-interactive authentication flows." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -683,7 +691,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 UpdateHumanUser + // Change user name + // + // Deprecated: use [user service v2 UpdateUser](apis/resources/user_service_v2/user-service-update-user.api.mdx) instead. + // + // Change the username of the user. Be aware that the user has to log in with the newly added username afterward rpc UpdateUserName(UpdateUserNameRequest) returns (UpdateUserNameResponse) { option (google.api.http) = { put: "/users/{user_id}/username" @@ -695,8 +707,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Change user name"; - description: "Change the username of the user. Be aware that the user has to log in with the newly added username afterward.\n\nDeprecated: please use user service v2 UpdateHumanUser" tags: "Users"; deprecated: true; responses: { @@ -903,7 +913,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 UpdateHumanUser + // Update User Profile (Human) + // + // Deprecated: use [user service v2 UpdateHumanUser](apis/resources/user_service_v2/user-service-update-human-user.api.mdx) instead. + // + // Update the profile information from a user. The profile includes basic information like first_name and last_name. rpc UpdateHumanProfile(UpdateHumanProfileRequest) returns (UpdateHumanProfileResponse) { option (google.api.http) = { put: "/users/{user_id}/profile" @@ -915,11 +929,9 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User Profile (Human)"; - description: "Update the profile information from a user. The profile includes basic information like first_name and last_name.\n\nDeprecated: please use user service v2 UpdateHumanUser" + deprecated: true; tags: "Users"; tags: "User Human"; - deprecated: true; responses: { key: "200" value: { @@ -970,7 +982,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetEmail + // Update User Email (Human) + // + // Deprecated: use [user service v2 SetEmail](apis/resources/user_service_v2/user-service-set-email.api.mdx) instead. + // + // Change the email address of a user. If the state is set to not verified, the user will get a verification email. rpc UpdateHumanEmail(UpdateHumanEmailRequest) returns (UpdateHumanEmailResponse) { option (google.api.http) = { put: "/users/{user_id}/email" @@ -982,8 +998,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User Email (Human)"; - description: "Change the email address of a user. If the state is set to not verified, the user will get a verification email.\n\nDeprecated: please use user service v2 SetEmail" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1039,7 +1053,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 ResendEmailCode + // Resend User Email Verification + // + // Deprecated: use [user service v2 ResendEmailCode](apis/resources/user_service_v2/user-service-resend-email-code.api.mdx) instead. + // + // Resend the email verification notification to the given email address of the user. rpc ResendHumanEmailVerification(ResendHumanEmailVerificationRequest) returns (ResendHumanEmailVerificationResponse) { option (google.api.http) = { post: "/users/{user_id}/email/_resend_verification" @@ -1051,8 +1069,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Resend User Email Verification"; - description: "Resend the email verification notification to the given email address of the user.\n\nDeprecated: please use user service v2 ResendEmailCode" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1106,7 +1122,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPhone + // Update User Phone (Human) + // + // Deprecated: use [user service v2 SetPhone](apis/resources/user_service_v2/user-service-update-user.api.mdx) instead. + // + // Change the phone number of a user. If the state is set to not verified, the user will get an SMS to verify (if a notification provider is configured). The phone number is only for informational purposes and to send messages, not for Authentication (2FA). rpc UpdateHumanPhone(UpdateHumanPhoneRequest) returns (UpdateHumanPhoneResponse) { option (google.api.http) = { put: "/users/{user_id}/phone" @@ -1118,8 +1138,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User Phone (Human)"; - description: "Change the phone number of a user. If the state is set to not verified, the user will get an SMS to verify (if a notification provider is configured). The phone number is only for informational purposes and to send messages, not for Authentication (2FA).\n\nDeprecated: please use user service v2 SetPhone" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1140,7 +1158,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPhone + // Remove User Phone (Human) + // + // Deprecated: use user service v2 [user service v2 SetPhone](apis/resources/user_service_v2/user-service-set-phone.api.mdx) instead. + // + // Remove the configured phone number of a user. rpc RemoveHumanPhone(RemoveHumanPhoneRequest) returns (RemoveHumanPhoneResponse) { option (google.api.http) = { delete: "/users/{user_id}/phone" @@ -1151,8 +1173,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Remove User Phone (Human)"; - description: "Remove the configured phone number of a user.\n\nDeprecated: please use user service v2 SetPhone" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1173,7 +1193,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 ResendPhoneCode + // Resend User Phone Verification + // + // Deprecated: use user service v2 [user service v2 ResendPhoneCode](apis/resources/user_service_v2/user-service-resend-phone-code.api.mdx) instead. + // + // Resend the notification for the verification of the phone number, to the number stored on the user. rpc ResendHumanPhoneVerification(ResendHumanPhoneVerificationRequest) returns (ResendHumanPhoneVerificationResponse) { option (google.api.http) = { post: "/users/{user_id}/phone/_resend_verification" @@ -1185,8 +1209,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Resend User Phone Verification"; - description: "Resend the notification for the verification of the phone number, to the number stored on the user.\n\nDeprecated: please use user service v2 ResendPhoneCode" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1238,7 +1260,9 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPassword + // Set Human Initial Password + // + // Deprecated: use [user service v2 SetPassword](apis/resources/user_service_v2/user-service-set-password.api.mdx) instead. rpc SetHumanInitialPassword(SetHumanInitialPasswordRequest) returns (SetHumanInitialPasswordResponse) { option (google.api.http) = { post: "/users/{user_id}/password/_initialize" @@ -1252,7 +1276,6 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Users"; tags: "User Human"; - summary: "Set Human Initial Password\n\nDeprecated: please use user service v2 SetPassword"; deprecated: true; parameters: { headers: { @@ -1265,7 +1288,9 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPassword + // Set User Password + // + // Deprecated: use [user service v2 SetPassword](apis/resources/user_service_v2/user-service-set-password.api.mdx) instead. rpc SetHumanPassword(SetHumanPasswordRequest) returns (SetHumanPasswordResponse) { option (google.api.http) = { post: "/users/{user_id}/password" @@ -1277,8 +1302,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set User Password"; - description: "Set a new password for a user. Per default, the user has to change the password on the next login. You can set no_change_required to true, to avoid the change on the next login.\n\nDeprecated: please use user service v2 SetPassword" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1299,7 +1322,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 PasswordReset + // Send Reset Password Notification + // + // Deprecated: use [user service v2 PasswordReset](apis/resources/user_service_v2/user-service-password-reset.api.mdx) instead. + // + // The user will receive an email with a link to change the password. rpc SendHumanResetPasswordNotification(SendHumanResetPasswordNotificationRequest) returns (SendHumanResetPasswordNotificationResponse) { option (google.api.http) = { post: "/users/{user_id}/password/_reset" @@ -1311,8 +1338,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Send Reset Password Notification"; - description: "The user will receive an email with a link to change the password.\n\nDeprecated: please use user service v2 PasswordReset" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1629,6 +1654,11 @@ service ManagementService { }; } + // Update Machine User + // + // Deprecated: use [user service v2 UpdateUser](apis/resources/user_service_v2/user-service-update-user.api.mdx) to update a user of type machine instead. + // + // Change a service account/machine user. It is used for accounts with non-interactive authentication possibilities. rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse) { option (google.api.http) = { put: "/users/{user_id}/machine" @@ -1640,8 +1670,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update Machine User"; - description: "Change a service account/machine user. It is used for accounts with non-interactive authentication possibilities." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1661,6 +1690,11 @@ service ManagementService { }; } + // Create Secret for Machine User + // + // Deprecated: use [user service v2 AddSecret](apis/resources/user_service_v2/user-service-add-secret.api.mdx) instead. + // + // Create a new secret for a machine user/service account. It is used to authenticate the user (client credential grant). rpc GenerateMachineSecret(GenerateMachineSecretRequest) returns (GenerateMachineSecretResponse) { option (google.api.http) = { put: "/users/{user_id}/secret" @@ -1672,8 +1706,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create Secret for Machine User"; - description: "Create a new secret for a machine user/service account. It is used to authenticate the user (client credential grant)." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1693,6 +1726,11 @@ service ManagementService { }; } + // Delete Secret of Machine User + // + // Deprecated: use [user service v2 RemoveSecret](apis/resources/user_service_v2/user-service-remove-secret.api.mdx) instead. + // + // Delete a secret of a machine user/service account. The user will not be able to authenticate with the secret afterward. rpc RemoveMachineSecret(RemoveMachineSecretRequest) returns (RemoveMachineSecretResponse) { option (google.api.http) = { delete: "/users/{user_id}/secret" @@ -1703,8 +1741,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete Secret of Machine User"; - description: "Delete a secret of a machine user/service account. The user will not be able to authenticate with the secret afterward." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1724,6 +1761,11 @@ service ManagementService { }; } + // Get Machine user Key By ID + // + // Deprecated: use [user service v2 ListKeys](apis/resources/user_service_v2/user-service-list-keys.api.mdx) instead. + // + // Get a specific Key of a machine user by its id. Machine keys are used to authenticate with jwt profile authentication. rpc GetMachineKeyByIDs(GetMachineKeyByIDsRequest) returns (GetMachineKeyByIDsResponse) { option (google.api.http) = { get: "/users/{user_id}/keys/{key_id}" @@ -1734,8 +1776,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get Machine user Key By ID"; - description: "Get a specific Key of a machine user by its id. Machine keys are used to authenticate with jwt profile authentication." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1755,6 +1796,11 @@ service ManagementService { }; } + // Get Machine user Key By ID + // + // Deprecated: use [user service v2 ListKeys](apis/resources/user_service_v2/user-service-list-keys.api.mdx) instead. + // + // Get the list of keys of a machine user. Machine keys are used to authenticate with jwt profile authentication. rpc ListMachineKeys(ListMachineKeysRequest) returns (ListMachineKeysResponse) { option (google.api.http) = { post: "/users/{user_id}/keys/_search" @@ -1766,8 +1812,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get Machine user Key By ID"; - description: "Get the list of keys of a machine user. Machine keys are used to authenticate with jwt profile authentication." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1787,6 +1832,14 @@ service ManagementService { }; } + // Create Key for machine user + // + // Deprecated: use [user service v2 AddKey](apis/resources/user_service_v2/user-service-add-key.api.mdx) instead. + // + // If a public key is not supplied, a new key is generated and will be returned in the response. + // Make sure to store the returned key. + // If an RSA public key is supplied, the private key is omitted from the response. + // Machine keys are used to authenticate with jwt profile. rpc AddMachineKey(AddMachineKeyRequest) returns (AddMachineKeyResponse) { option (google.api.http) = { post: "/users/{user_id}/keys" @@ -1798,8 +1851,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create Key for machine user"; - description: "If a public key is not supplied, a new key is generated and will be returned in the response. Make sure to store the returned key. If an RSA public key is supplied, the private key is omitted from the response. Machine keys are used to authenticate with jwt profile." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1819,6 +1871,12 @@ service ManagementService { }; } + // Delete Key for machine user + // + // Deprecated: use [user service v2 RemoveKey](apis/resources/user_service_v2/user-service-remove-key.api.mdx) instead. + // + // Delete a specific key from a user. + // The user will not be able to authenticate with that key afterward. rpc RemoveMachineKey(RemoveMachineKeyRequest) returns (RemoveMachineKeyResponse) { option (google.api.http) = { delete: "/users/{user_id}/keys/{key_id}" @@ -1829,8 +1887,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete Key for machine user"; - description: "Delete a specific key from a user. The user will not be able to authenticate with that key afterward." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1850,6 +1907,11 @@ service ManagementService { }; } + // Get Personal-Access-Token (PAT) by ID + // + // Deprecated: use [user service v2 ListPersonalAccessTokens](apis/resources/user_service_v2/user-service-list-personal-access-tokens.api.mdx) instead. + // + // Returns the PAT for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header. rpc GetPersonalAccessTokenByIDs(GetPersonalAccessTokenByIDsRequest) returns (GetPersonalAccessTokenByIDsResponse) { option (google.api.http) = { get: "/users/{user_id}/pats/{token_id}" @@ -1860,8 +1922,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get a Personal-Access-Token (PAT) by ID"; - description: "Returns the PAT for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1881,6 +1942,11 @@ service ManagementService { }; } + // List Personal-Access-Tokens (PATs) + // + // Deprecated: use [user service v2 ListPersonalAccessTokens](apis/resources/user_service_v2/user-service-list-personal-access-tokens.api.mdx) instead. + // + // Returns a list of PATs for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header. rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) { option (google.api.http) = { post: "/users/{user_id}/pats/_search" @@ -1892,8 +1958,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get a Personal-Access-Token (PAT) by ID"; - description: "Returns a list of PATs for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1913,6 +1978,13 @@ service ManagementService { }; } + // Create a Personal-Access-Token (PAT) + // + // Deprecated: use [user service v2 AddPersonalAccessToken](apis/resources/user_service_v2/user-service-add-personal-access-token.api.mdx) instead. + // + // Generates a new PAT for the user. Currently only available for machine users. + // The token will be returned in the response, make sure to store it. + // PATs are ready-to-use tokens and can be sent directly in the authentication header. rpc AddPersonalAccessToken(AddPersonalAccessTokenRequest) returns (AddPersonalAccessTokenResponse) { option (google.api.http) = { post: "/users/{user_id}/pats" @@ -1924,8 +1996,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create a Personal-Access-Token (PAT)"; - description: "Generates a new PAT for the user. Currently only available for machine users. The token will be returned in the response, make sure to store it. PATs are ready-to-use tokens and can be sent directly in the authentication header." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1945,6 +2016,11 @@ service ManagementService { }; } + // Remove a Personal-Access-Token (PAT) by ID + // + // Deprecated: use [user service v2 RemovePersonalAccessToken](apis/resources/user_service_v2/user-service-remove-personal-access-token.api.mdx) instead. + // + // Delete a PAT from a user. Afterward, the user will not be able to authenticate with that token anymore. rpc RemovePersonalAccessToken(RemovePersonalAccessTokenRequest) returns (RemovePersonalAccessTokenResponse) { option (google.api.http) = { delete: "/users/{user_id}/pats/{token_id}" @@ -1955,8 +2031,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get a Personal-Access-Token (PAT) by ID"; - description: "Delete a PAT from a user. Afterward, the user will not be able to authenticate with that token anymore." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -2003,7 +2078,7 @@ service ManagementService { }; } - // Deprecated: please use user service v2 RemoveLinkedIDP + // Deprecated: please use [user service v2 RemoveIDPLink](apis/resources/user_service_v2/user-service-remove-idp-link.api.mdx) rpc RemoveHumanLinkedIDP(RemoveHumanLinkedIDPRequest) returns (RemoveHumanLinkedIDPResponse) { option (google.api.http) = { delete: "/users/{user_id}/idps/{idp_id}/{linked_user_id}" @@ -2119,6 +2194,7 @@ service ManagementService { }; } + // Deprecated: use CreateOrganization [apis/resources/org_service_v2beta/organization-service-create-organization.api.mdx] API instead rpc AddOrg(AddOrgRequest) returns (AddOrgResponse) { option (google.api.http) = { post: "/orgs" @@ -2133,6 +2209,7 @@ service ManagementService { tags: "Organizations"; summary: "Create Organization"; description: "Create a new organization. Based on the given name a domain will be generated to be able to identify users within an organization." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2144,6 +2221,7 @@ service ManagementService { }; } + // Deprecated: use UpdateOrganization [apis/resources/org_service_v2beta/organization-service-update-organization.api.mdx] API instead rpc UpdateOrg(UpdateOrgRequest) returns (UpdateOrgResponse) { option (google.api.http) = { put: "/orgs/me" @@ -2158,6 +2236,7 @@ service ManagementService { tags: "Organizations"; summary: "Update Organization"; description: "Change the name of the organization." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2169,6 +2248,7 @@ service ManagementService { }; } + // Deprecated: use DeactivateOrganization [apis/resources/org_service_v2beta/organization-service-deactivate-organization.api.mdx] API instead rpc DeactivateOrg(DeactivateOrgRequest) returns (DeactivateOrgResponse) { option (google.api.http) = { post: "/orgs/me/_deactivate" @@ -2183,6 +2263,7 @@ service ManagementService { tags: "Organizations"; summary: "Deactivate Organization"; description: "Sets the state of my organization to deactivated. Users of this organization will not be able to log in." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2194,6 +2275,7 @@ service ManagementService { }; } + // Deprecated: use ActivateOrganization [apis/resources/org_service_v2beta/organization-service-activate-organization.api.mdx] API instead rpc ReactivateOrg(ReactivateOrgRequest) returns (ReactivateOrgResponse) { option (google.api.http) = { post: "/orgs/me/_reactivate" @@ -2208,6 +2290,7 @@ service ManagementService { tags: "Organizations"; summary: "Reactivate Organization"; description: "Set the state of my organization to active. The state of the organization has to be deactivated to perform the request. Users of this organization will be able to log in again." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2219,6 +2302,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganization [apis/resources/org_service_v2beta/organization-service-delete-organization.api.mdx] API instead rpc RemoveOrg(RemoveOrgRequest) returns (RemoveOrgResponse) { option (google.api.http) = { delete: "/orgs/me" @@ -2232,6 +2316,7 @@ service ManagementService { tags: "Organizations"; summary: "Delete Organization"; description: "Deletes my organization and all its resources (Users, Projects, Grants to and from the org). Users of this organization will not be able to log in." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2243,6 +2328,7 @@ service ManagementService { }; } + // Deprecated: use SetOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-set-organization-metadata.api.mdx] API instead rpc SetOrgMetadata(SetOrgMetadataRequest) returns (SetOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/{key}" @@ -2258,6 +2344,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Set Organization Metadata"; description: "This endpoint either adds or updates a metadata value for the requested key. Make sure the value is base64 encoded." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2269,6 +2356,7 @@ service ManagementService { }; } + // Deprecated: use SetOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-set-organization-metadata.api.mdx] API instead rpc BulkSetOrgMetadata(BulkSetOrgMetadataRequest) returns (BulkSetOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/_bulk" @@ -2284,6 +2372,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Bulk Set Organization Metadata"; description: "This endpoint sets a list of metadata to the organization. Make sure the values are base64 encoded." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2295,6 +2384,7 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-list-organization-metadata.api.mdx] API instead rpc ListOrgMetadata(ListOrgMetadataRequest) returns (ListOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/_search" @@ -2310,6 +2400,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Search Organization Metadata"; description: "Get the metadata of an organization filtered by your query." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2321,6 +2412,7 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-list-organization-metadata.api.mdx] API instead rpc GetOrgMetadata(GetOrgMetadataRequest) returns (GetOrgMetadataResponse) { option (google.api.http) = { get: "/metadata/{key}" @@ -2335,6 +2427,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Get Organization Metadata By Key"; description: "Get a metadata object from an organization by a specific key." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2346,6 +2439,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-delete-organization-metadata.api.mdx] API instead rpc RemoveOrgMetadata(RemoveOrgMetadataRequest) returns (RemoveOrgMetadataResponse) { option (google.api.http) = { delete: "/metadata/{key}" @@ -2360,6 +2454,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Delete Organization Metadata By Key"; description: "Remove a metadata object from an organization with a specific key." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2371,6 +2466,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-delete-organization-metadata.api.mdx] API instead rpc BulkRemoveOrgMetadata(BulkRemoveOrgMetadataRequest) returns (BulkRemoveOrgMetadataResponse) { option (google.api.http) = { delete: "/metadata/_bulk" @@ -2384,6 +2480,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Organizations"; tags: "Organization Metadata"; + deprecated: true summary: "Bulk Delete Metadata"; description: "Remove a list of metadata objects from an organization with a list of keys." parameters: { @@ -2397,31 +2494,7 @@ service ManagementService { }; } - rpc ListOrgDomains(ListOrgDomainsRequest) returns (ListOrgDomainsResponse) { - option (google.api.http) = { - post: "/orgs/me/domains/_search" - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "org.read" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Organizations"; - summary: "Search Domains"; - description: "Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs." - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; - } - + // Deprecated: use AddOrganizationDomain [apis/resources/org_service_v2beta/organization-service-add-organization-domain.api.mdx] API instead rpc AddOrgDomain(AddOrgDomainRequest) returns (AddOrgDomainResponse) { option (google.api.http) = { post: "/orgs/me/domains" @@ -2436,6 +2509,7 @@ service ManagementService { tags: "Organizations"; summary: "Add Domain"; description: "Add a new domain to an organization. The domains are used to identify to which organization a user belongs." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2447,6 +2521,34 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationDomains [apis/resources/org_service_v2beta/organization-service-list-organization-domains.api.mdx] API instead + rpc ListOrgDomains(ListOrgDomainsRequest) returns (ListOrgDomainsResponse) { + option (google.api.http) = { + post: "/orgs/me/domains/_search" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.read" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Organizations"; + summary: "Search Domains"; + description: "Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs." + deprecated: true + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + }; + } + + // Deprecated: use DeleteOrganizationDomain [apis/resources/org_service_v2beta/organization-service-delete-organization-domain.api.mdx] API instead rpc RemoveOrgDomain(RemoveOrgDomainRequest) returns (RemoveOrgDomainResponse) { option (google.api.http) = { delete: "/orgs/me/domains/{domain}" @@ -2460,6 +2562,7 @@ service ManagementService { tags: "Organizations"; summary: "Remove Domain"; description: "Delete a new domain from an organization. The domains are used to identify to which organization a user belongs. If the uses use the domain for login, this will not be possible afterwards. They have to use another domain instead." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2471,6 +2574,7 @@ service ManagementService { }; } + // Deprecated: use GenerateOrganizationDomainValidation [apis/resources/org_service_v2beta/organization-service-generate-organization-domain-validation.api.mdx] API instead rpc GenerateOrgDomainValidation(GenerateOrgDomainValidationRequest) returns (GenerateOrgDomainValidationResponse) { option (google.api.http) = { post: "/orgs/me/domains/{domain}/validation/_generate" @@ -2485,6 +2589,7 @@ service ManagementService { tags: "Organizations"; summary: "Generate Domain Verification"; description: "Generate a new file to be able to verify your domain with DNS or HTTP challenge." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2496,6 +2601,7 @@ service ManagementService { }; } + // Deprecated: use VerifyOrganizationDomain [apis/resources/org_service_v2beta/organization-service-verify-organization-domain.api.mdx] API instead rpc ValidateOrgDomain(ValidateOrgDomainRequest) returns (ValidateOrgDomainResponse) { option (google.api.http) = { post: "/orgs/me/domains/{domain}/validation/_validate" @@ -2510,6 +2616,7 @@ service ManagementService { tags: "Organizations"; summary: "Verify Domain"; description: "Make sure you have added the required verification to your domain, depending on the method you have chosen (HTTP or DNS challenge). ZITADEL will check it and set the domain as verified if it was successful. A verify domain has to be unique." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -9209,7 +9316,7 @@ message AddOrgMemberRequest { repeated string roles = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[\"ORG_OWNER\"]"; - description: "If no roles are provided the user won't have any rights" + description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid." } ]; } @@ -9222,7 +9329,7 @@ message UpdateOrgMemberRequest { repeated string roles = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[\"IAM_OWNER\"]"; - description: "If no roles are provided the user won't have any rights" + description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid." } ]; } @@ -9643,7 +9750,7 @@ message AddProjectMemberRequest { repeated string roles = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[\"PROJECT_OWNER\"]"; - description: "If no roles are provided the user won't have any rights" + description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid." } ]; } @@ -9658,7 +9765,7 @@ message UpdateProjectMemberRequest { repeated string roles = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[\"PROJECT_OWNER\"]"; - description: "If no roles are provided the user won't have any rights" + description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid." } ]; } @@ -10313,7 +10420,7 @@ message AddProjectGrantMemberRequest { repeated string roles = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[\"PROJECT_GRANT_OWNER\"]"; - description: "If no roles are provided the user won't have any rights" + description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid." } ]; } @@ -10337,7 +10444,7 @@ message UpdateProjectGrantMemberRequest { repeated string roles = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[\"PROJECT_GRANT_OWNER\"]"; - description: "If no roles are provided the user won't have any rights" + description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid." } ]; } @@ -13320,6 +13427,9 @@ message AddSAMLProviderRequest { // Optionally specify the 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`. optional string transient_mapping_attribute_name = 8; + // Optionally enable federated logout. If enabled, ZITADEL will send a logout request to the identity provider, + // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata. + optional bool federated_logout_enabled = 9; } message AddSAMLProviderResponse { @@ -13354,6 +13464,9 @@ message UpdateSAMLProviderRequest { // Optionally specify the 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`. optional string transient_mapping_attribute_name = 9; + // Optionally enable federated logout. If enabled, ZITADEL will send a logout request to the identity provider, + // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata. + optional bool federated_logout_enabled = 10; } message UpdateSAMLProviderResponse { diff --git a/proto/zitadel/metadata/v2beta/metadata.proto b/proto/zitadel/metadata/v2beta/metadata.proto new file mode 100644 index 0000000000..87fcc51869 --- /dev/null +++ b/proto/zitadel/metadata/v2beta/metadata.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +import "zitadel/object/v2beta/object.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +package zitadel.metadata.v2beta; + +option go_package ="github.com/zitadel/zitadel/pkg/grpc/metadata/v2beta"; + +message Metadata { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + string key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata key", + example: "\"key1\""; + } + ]; + bytes value = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata value is base64 encoded, make sure to decode to get the value", + example: "\"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\""; + } + ]; +} + +message MetadataQuery { + oneof query { + option (validate.required) = true; + MetadataKeyQuery key_query = 1; + } +} + +message MetadataKeyQuery { + string key = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"key\"" + } + ]; + zitadel.object.v2beta.TextQueryMethod 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/proto/zitadel/org/v2/org_service.proto b/proto/zitadel/org/v2/org_service.proto index 94ced55146..729350e1f9 100644 --- a/proto/zitadel/org/v2/org_service.proto +++ b/proto/zitadel/org/v2/org_service.proto @@ -197,6 +197,14 @@ message AddOrganizationRequest{ } ]; repeated Admin admins = 2; + // optionally set your own id unique for the organization. + optional string org_id = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; } message AddOrganizationResponse{ diff --git a/proto/zitadel/org/v2beta/org.proto b/proto/zitadel/org/v2beta/org.proto new file mode 100644 index 0000000000..08cf47e820 --- /dev/null +++ b/proto/zitadel/org/v2beta/org.proto @@ -0,0 +1,169 @@ +syntax = "proto3"; + +package zitadel.org.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2beta;org"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/object/v2beta/object.proto"; +import "google/protobuf/timestamp.proto"; + +message Organization { + // Unique identifier of the organization. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + + // The timestamp of the organization was created. + 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 verification of the organization domain. + google.protobuf.Timestamp changed_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + // Current state of the organization, for example active, inactive and deleted. + OrgState state = 4; + + // Name of the organization. + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + // Primary domain used in the organization. + string primary_domain = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; +} + +enum OrgState { + ORG_STATE_UNSPECIFIED = 0; + ORG_STATE_ACTIVE = 1; + ORG_STATE_INACTIVE = 2; + ORG_STATE_REMOVED = 3; +} + +enum OrgFieldName { + ORG_FIELD_NAME_UNSPECIFIED = 0; + ORG_FIELD_NAME_NAME = 1; + ORG_FIELD_NAME_CREATION_DATE = 2; +} + +message OrganizationSearchFilter{ + oneof filter { + option (validate.required) = true; + + OrgNameFilter name_filter = 1; + OrgDomainFilter domain_filter = 2; + OrgStateFilter state_filter = 3; + OrgIDFilter id_filter = 4; + } +} +message OrgNameFilter { + // Organization name. + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgDomainFilter { + // The domain. + string domain = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgStateFilter { + // Current state of the organization. + OrgState state = 1 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgIDFilter { + // The Organization id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; +} + +// from proto/zitadel/org.proto +message DomainSearchFilter { + oneof filter { + option (validate.required) = true; + DomainNameFilter domain_name_filter = 1; + } +} + +// from proto/zitadel/org.proto +message DomainNameFilter { + // The domain. + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +// from proto/zitadel/org.proto +message Domain { + // The Organization id. + string organization_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The domain name. + string domain_name = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\""; + } + ]; + // Defines if the domain is verified. + bool is_verified = 3; + // Defines if the domain is the primary domain. + bool is_primary = 4; + // Defines the protocol the domain was validated with. + DomainValidationType validation_type = 5; +} + +// from proto/zitadel/org.proto +enum DomainValidationType { + DOMAIN_VALIDATION_TYPE_UNSPECIFIED = 0; + DOMAIN_VALIDATION_TYPE_HTTP = 1; + DOMAIN_VALIDATION_TYPE_DNS = 2; +} diff --git a/proto/zitadel/org/v2beta/org_service.proto b/proto/zitadel/org/v2beta/org_service.proto index 90c29ca354..387b2cb825 100644 --- a/proto/zitadel/org/v2beta/org_service.proto +++ b/proto/zitadel/org/v2beta/org_service.proto @@ -6,24 +6,22 @@ package zitadel.org.v2beta; import "zitadel/object/v2beta/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; import "zitadel/user/v2beta/auth.proto"; -import "zitadel/user/v2beta/email.proto"; -import "zitadel/user/v2beta/phone.proto"; -import "zitadel/user/v2beta/idp.proto"; -import "zitadel/user/v2beta/password.proto"; -import "zitadel/user/v2beta/user.proto"; +import "zitadel/org/v2beta/org.proto"; +import "zitadel/metadata/v2beta/metadata.proto"; import "zitadel/user/v2beta/user_service.proto"; 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 "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2beta;org"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { - title: "User Service"; + title: "Organization Service (Beta)"; version: "2.0-beta"; description: "This API is intended to manage organizations in a ZITADEL instance. This project is in beta state. It can AND will continue breaking until the services provide the same functionality as the current login."; contact:{ @@ -111,8 +109,13 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { service OrganizationService { - // Create a new organization and grant the user(s) permission to manage it - rpc AddOrganization(AddOrganizationRequest) returns (AddOrganizationResponse) { + // Create Organization + // + // Create a new organization with an administrative user. If no specific roles are sent for the users, they will be granted the role ORG_OWNER. + // + // Required permission: + // - `org.create` + rpc CreateOrganization(CreateOrganizationRequest) returns (CreateOrganizationResponse) { option (google.api.http) = { post: "/v2beta/organizations" body: "*" @@ -122,34 +125,411 @@ service OrganizationService { auth_option: { permission: "org.create" } - http_response: { - success_code: 201 - } }; - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create an Organization"; - description: "Create a new organization with an administrative user. If no specific roles are sent for the users, they will be granted the role ORG_OWNER." + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { responses: { - key: "200" + key: "200"; value: { - description: "OK"; + description: "Organization created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The organization to create already exists."; } }; }; } + + // Update Organization + // + // Change the name of the organization. + // + // Required permission: + // - `org.write` + rpc UpdateOrganization(UpdateOrganizationRequest) returns (UpdateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Organization created successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "Organisation's not found"; + } + }; + responses: { + key: "409" + value: { + description: "Organisation's name already taken"; + } + }; + }; + + } + + // List Organizations + // + // Returns a list of organizations that match the requesting filters. All filters are applied with an AND condition. + // + // Required permission: + // - `iam.read` + rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/search"; + body: "*"; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read"; + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } + + // Delete Organization + // + // Deletes the organization and all its resources (Users, Projects, Grants to and from the org). Users of this organization will not be able to log in. + // + // Required permission: + // - `org.delete` + rpc DeleteOrganization(DeleteOrganizationRequest) returns (DeleteOrganizationResponse) { + option (google.api.http) = { + delete: "/v2beta/organizations/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.delete"; + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Organization created successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "Organisation's not found"; + } + }; + }; + } + + // Set Organization Metadata + // + // Adds or updates a metadata value for the requested key. Make sure the value is base64 encoded. + // + // Required permission: + // - `org.write` + rpc SetOrganizationMetadata(SetOrganizationMetadataRequest) returns (SetOrganizationMetadataResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/metadata" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + // TODO This needs to chagne to 404 + key: "400" + value: { + description: "Organisation's not found"; + } + }; + }; + } + + // List Organization Metadata + // + // List metadata of an organization filtered by query. + // + // Required permission: + // - `org.read` + rpc ListOrganizationMetadata(ListOrganizationMetadataRequest) returns (ListOrganizationMetadataResponse ) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/metadata/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { + permission: "org.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Delete Organization Metadata + // + // Delete metadata objects from an organization with a specific key. + // + // Required permission: + // - `org.write` + rpc DeleteOrganizationMetadata(DeleteOrganizationMetadataRequest) returns (DeleteOrganizationMetadataResponse) { + option (google.api.http) = { + delete: "/v2beta/organizations/{organization_id}/metadata" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Add Organization Domain + // + // Add a new domain to an organization. The domains are used to identify to which organization a user belongs. + // + // Required permission: + // - `org.write` + rpc AddOrganizationDomain(AddOrganizationDomainRequest) returns (AddOrganizationDomainResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "409" + value: { + description: "Domain already exists"; + } + }; + }; + + } + + // List Organization Domains + // + // Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs. + // + // Required permission: + // - `org.read` + rpc ListOrganizationDomains(ListOrganizationDomainsRequest) returns (ListOrganizationDomainsResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Delete Organization Domain + // + // Delete a new domain from an organization. The domains are used to identify to which organization a user belongs. If the uses use the domain for login, this will not be possible afterwards. They have to use another domain instead. + // + // Required permission: + // - `org.write` + rpc DeleteOrganizationDomain(DeleteOrganizationDomainRequest) returns (DeleteOrganizationDomainResponse) { + option (google.api.http) = { + delete: "/v2beta/organizations/{organization_id}/domains" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Generate Organization Domain Validation + // + // Generate a new file to be able to verify your domain with DNS or HTTP challenge. + // + // Required permission: + // - `org.write` + rpc GenerateOrganizationDomainValidation(GenerateOrganizationDomainValidationRequest) returns (GenerateOrganizationDomainValidationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains/validation/generate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "404" + value: { + description: "Domain doesn't exist on organization"; + } + }; + }; + } + + // Verify Organization Domain + // + // Make sure you have added the required verification to your domain, depending on the method you have chosen (HTTP or DNS challenge). ZITADEL will check it and set the domain as verified if it was successful. A verify domain has to be unique. + // + // Required permission: + // - `org.write` + rpc VerifyOrganizationDomain(VerifyOrganizationDomainRequest) returns (VerifyOrganizationDomainResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains/validation/verify" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } + + // Deactivate Organization + // + // Sets the state of my organization to deactivated. Users of this organization will not be able to log in. + // + // Required permission: + // - `org.write` + rpc DeactivateOrganization(DeactivateOrganizationRequest) returns (DeactivateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}/deactivate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Activate Organization + // + // Set the state of my organization to active. The state of the organization has to be deactivated to perform the request. Users of this organization will be able to log in again. + // + // Required permission: + // - `org.write` + rpc ActivateOrganization(ActivateOrganizationRequest) returns (ActivateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}/activate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + } -message AddOrganizationRequest{ +message CreateOrganizationRequest{ + // The Admin for the newly created Organization. message Admin { oneof user_type{ string user_id = 1; zitadel.user.v2beta.AddHumanUserRequest human = 2; } - // specify Org Member Roles for the provided user (default is ORG_OWNER if roles are empty) + // specify Organization Member Roles for the provided user (default is ORG_OWNER if roles are empty) repeated string roles = 3; } + // name of the Organization to be created. string name = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, @@ -159,16 +539,417 @@ message AddOrganizationRequest{ example: "\"ZITADEL\""; } ]; - repeated Admin admins = 2; + // Optionally set your own id unique for the organization. + optional 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: "\"69629012906488334\""; + } + ]; + // Additional Admins for the Organization. + repeated Admin admins = 3; } -message AddOrganizationResponse{ - message CreatedAdmin { - string user_id = 1; - optional string email_code = 2; - optional string phone_code = 3; - } - zitadel.object.v2beta.Details details = 1; - string organization_id = 2; - repeated CreatedAdmin created_admins = 3; +message CreatedAdmin { + string user_id = 1; + optional string email_code = 2; + optional string phone_code = 3; } + +message AssignedAdmin { + string user_id = 1; +} + +message OrganizationAdmin { + // The admins created/assigned for the Organization. + oneof OrganizationAdmin { + CreatedAdmin created_admin = 1; + AssignedAdmin assigned_admin = 2; + } +} + +message CreateOrganizationResponse{ + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // Organization ID of the newly created organization. + 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: "\"69629012906488334\""; + } + ]; + + // The admins created/assigned for the Organization + repeated OrganizationAdmin organization_admins = 3; +} + +message UpdateOrganizationRequest { + // Organization Id for the Organization to be updated + 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: "\"69629012906488334\""; + } + ]; + + // New Name for the Organization to be updated + string name = 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: "\"Customer 1\""; + } + ]; +} + +message UpdateOrganizationResponse { + // The timestamp of the update to the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListOrganizationsRequest { + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // the field the result is sorted + zitadel.org.v2beta.OrgFieldName sorting_column = 2; + // Define the criteria to query for. + // repeated ProjectRoleQuery filters = 4; + repeated zitadel.org.v2beta.OrganizationSearchFilter filter = 3; +} + +message ListOrganizationsResponse { + // Pagination of the Organizations results + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The Organizations requested + repeated zitadel.org.v2beta.Organization organizations = 2; +} + +message DeleteOrganizationRequest { + + // Organization Id for the Organization to be deleted + 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) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message DeleteOrganizationResponse { + // The timestamp of the deletion of the organization. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeactivateOrganizationRequest { + // Organization Id for the Organization to be deactivated + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message DeactivateOrganizationResponse { + // The timestamp of the deactivation of the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ActivateOrganizationRequest { + // Organization Id for the Organization to be activated + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message ActivateOrganizationResponse { + // The timestamp of the activation of the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message AddOrganizationDomainRequest { + // Organization Id for the Organization for which the domain is to be added to. + string organization_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: "\"69629012906488334\""; + } + ]; + // The domain you want to add to the organization. + string domain = 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: "\"testdomain.com\""; + } + ]; +} + +message AddOrganizationDomainResponse { + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ListOrganizationDomainsRequest { + // Organization Id for the Organization which domains are to be listed. + string organization_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: "\"69629012906488334\""; + } + ]; + + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 2; + // Define the criteria to query for. + repeated DomainSearchFilter filters = 3; +} + +message ListOrganizationDomainsResponse { + // Pagination of the Organizations domain results. + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The domains requested. + repeated Domain domains = 2; +} + +message DeleteOrganizationDomainRequest { + // Organization Id for the Organization which domain is to be deleted. + string organization_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: "\"69629012906488334\""; + } + ]; + string domain = 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: "\"testdomain.com\""; + } + ]; +} + +message DeleteOrganizationDomainResponse { + // The timestamp of the deletion of the organization domain. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GenerateOrganizationDomainValidationRequest { + // Organization Id for the Organization which doman to be validated. + string organization_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: "\"69629012906488334\""; + } + ]; + // The domain which to be deleted. + string domain = 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: "\"testdomain.com\""; + } + ]; + DomainValidationType type = 3 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; +} + +message GenerateOrganizationDomainValidationResponse { + // The token verify domain. + string token = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ofSBHsSAVHAoTIE4Iv2gwhaYhTjcY5QX\""; + } + ]; + // URL used to verify the domain. + string url = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://testdomain.com/.well-known/zitadel-challenge/ofSBHsSAVHAoTIE4Iv2gwhaYhTjcY5QX\""; + } + ]; +} + +message VerifyOrganizationDomainRequest { + // Organization Id for the Organization doman to be verified. + string organization_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: "\"69629012906488334\""; + } + ]; + // Organization Id for the Organization doman to be verified. + string domain = 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: "\"testdomain.com\""; + } + ]; +} + +message VerifyOrganizationDomainResponse { + // The timestamp of the verification of the organization domain. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message Metadata { + // Key in the metadata key/value pair. + string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // Value in the metadata key/value pair. + bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}]; +} +message SetOrganizationMetadataRequest{ + // Organization Id for the Organization doman to be verified. + string organization_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: "\"69629012906488334\""; + } + ]; + // Metadata to set. + repeated Metadata metadata = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title: "Medata (Key/Value)" + description: "The values have to be base64 encoded."; + example: "[{\"key\": \"test1\", \"value\": \"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\"}, {\"key\": \"test2\", \"value\": \"VGhpcyBpcyBteSBzZWNvbmQgdmFsdWU=\"}]" + } + ]; +} + +message SetOrganizationMetadataResponse{ + // The timestamp of the update of the organization metadata. + google.protobuf.Timestamp set_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListOrganizationMetadataRequest { + // Organization ID of Orgalization which metadata is to be listed. + string organization_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: "\"69629012906488334\""; + } + ]; + + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 2; + // Define the criteria to query for. + repeated zitadel.metadata.v2beta.MetadataQuery filter = 3; +} + +message ListOrganizationMetadataResponse { + // Pagination of the Organizations metadata results. + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The Organization metadata requested. + repeated zitadel.metadata.v2beta.Metadata metadata = 2; +} + +message DeleteOrganizationMetadataRequest { + // Organization ID of Orgalization which metadata is to be deleted is stored on. + string organization_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: "\"69629012906488334\""; + } + ]; + // The keys for the Organization metadata to be deleted. + repeated string keys = 2 [(validate.rules).repeated.items.string = {min_len: 1, max_len: 200}]; +} + +message DeleteOrganizationMetadataResponse{ + // The timestamp of the deletiion of the organization metadata. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + + diff --git a/proto/zitadel/project/v2beta/project_service.proto b/proto/zitadel/project/v2beta/project_service.proto new file mode 100644 index 0000000000..cb7110bc91 --- /dev/null +++ b/proto/zitadel/project/v2beta/project_service.proto @@ -0,0 +1,1237 @@ +syntax = "proto3"; + +package zitadel.project.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/project/v2beta/query.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/project/v2beta;project"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Project Service"; + version: "2.0-beta"; + description: "This API is intended to manage Projects in a ZITADEL Organization. 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 projects. +service ProjectService { + + // Create Project + // + // Create a new Project. + // + // Required permission: + // - `project.create` + rpc CreateProject (CreateProjectRequest) returns (CreateProjectResponse) { + option (google.api.http) = { + post: "/v2beta/projects" + 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: "Project created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The project to create already exists."; + } + }; + }; + } + + // Update Project + // + // Update an existing project. + // + // Required permission: + // - `project.write` + rpc UpdateProject (UpdateProjectRequest) returns (UpdateProjectResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{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: "Project successfully updated or left unchanged"; + }; + }; + responses: { + key: "404" + value: { + description: "The project to update does not exist."; + } + }; + }; + } + + // Delete Project + // + // Delete an existing project. + // In case the project is not found, the request will return a successful response as + // the desired state is already achieved. + // + // Required permission: + // - `project.delete` + rpc DeleteProject (DeleteProjectRequest) returns (DeleteProjectResponse) { + option (google.api.http) = { + delete: "/v2beta/projects/{id}" + }; + + 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: "Project deleted successfully"; + }; + }; + }; + } + + // Get Project + // + // Returns the project identified by the requested ID. + // + // Required permission: + // - `project.read` + rpc GetProject (GetProjectRequest) returns (GetProjectResponse) { + option (google.api.http) = { + get: "/v2beta/projects/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Project retrieved successfully"; + } + }; + responses: { + key: "404" + value: { + description: "The project to get does not exist."; + } + }; + }; + } + + // List Projects + // + // List all matching projects. By default all projects of the instance that the caller has permission to read are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - `project.read` + rpc ListProjects (ListProjectsRequest) returns (ListProjectsResponse) { + option (google.api.http) = { + post: "/v2beta/projects/search", + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all projects matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // Deactivate Project + // + // Set the state of a project to deactivated. Request returns no error if the project is already deactivated. + // Applications under deactivated projects are not able to login anymore. + // + // Required permission: + // - `project.write` + rpc DeactivateProject (DeactivateProjectRequest) returns (DeactivateProjectResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{id}/deactivate" + 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: "Project deactivated successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project to deactivate does not exist."; + } + }; + }; + } + + // Activate Project + // + // Set the state of a project to active. Request returns no error if the project is already activated. + // + // Required permission: + // - `project.write` + rpc ActivateProject (ActivateProjectRequest) returns (ActivateProjectResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{id}/activate" + 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: "Project activated successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project to activate does not exist."; + } + }; + }; + } + + // Add Project Role + // + // Add a new project role to a project. The key must be unique within the project. + // + // Required permission: + // - `project.role.write` + rpc AddProjectRole (AddProjectRoleRequest) returns (AddProjectRoleResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/roles" + 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: "Project role added successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project to add the project role does not exist."; + } + }; + }; + } + + // Update Project Role + // + // Change a project role. The key is not editable. If a key should change, remove the role and create a new one. + // + // Required permission: + // - `project.role.write` + rpc UpdateProjectRole (UpdateProjectRoleRequest) returns (UpdateProjectRoleResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/roles/{role_key}" + 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: "Project role updated successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project role to update does not exist."; + } + }; + }; + } + + // Remove Project Role + // + // Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants. + // + // Required permission: + // - `project.role.write` + rpc RemoveProjectRole (RemoveProjectRoleRequest) returns (RemoveProjectRoleResponse) { + option (google.api.http) = { + delete: "/v2beta/projects/{project_id}/roles/{role_key}" + }; + + 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: "Project role removed successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project role to remove does not exist."; + } + }; + }; + } + + // List Project Roles + // + // Returns all roles of a project matching the search query. + // + // Required permission: + // - `project.role.read` + rpc ListProjectRoles (ListProjectRolesRequest) returns (ListProjectRolesResponse) { + option (google.api.http) = { + delete: "/v2beta/projects/{project_id}/roles/search" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.role.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all project roles matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // Create Project Grant + // + // Grant a project to another organization. + // The project grant will allow the granted organization to access the project and manage the authorizations for its users. + // + // Required permission: + // - `project.grant.create` + rpc CreateProjectGrant (CreateProjectGrantRequest) returns (CreateProjectGrantResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/grants" + 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: "Project grant created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The project grant to create already exists."; + } + }; + }; + } + + // Update Project Grant + // + // Change the roles of the project that is granted to another organization. + // The project grant will allow the granted organization to access the project and manage the authorizations for its users. + // + // Required permission: + // - `project.grant.write` + rpc UpdateProjectGrant (UpdateProjectGrantRequest) returns (UpdateProjectGrantResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/grants/{granted_organization_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: "Project grant successfully updated or left unchanged"; + }; + }; + responses: { + key: "404" + value: { + description: "The project grant to update does not exist."; + } + }; + }; + } + + // Delete Project Grant + // + // Delete a project grant. All user grants for this project grant will also be removed. + // A user will not have access to the project afterward (if permissions are checked). + // In case the project grant is not found, the request will return a successful response as + // the desired state is already achieved. + // + // Required permission: + // - `project.grant.delete` + rpc DeleteProjectGrant (DeleteProjectGrantRequest) returns (DeleteProjectGrantResponse) { + option (google.api.http) = { + delete: "/v2beta/projects/{project_id}/grants/{granted_organization_id}" + }; + + 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: "Project grant deleted successfully"; + }; + }; + }; + } + + // Deactivate Project Grant + // + // Set the state of the project grant to deactivated. + // Applications under deactivated projects grants are not able to login anymore. + // + // Required permission: + // - `project.grant.write` + rpc DeactivateProjectGrant(DeactivateProjectGrantRequest) returns (DeactivateProjectGrantResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/grants/{granted_organization_id}/deactivate" + 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: "Project grant deactivated successfully"; + }; + }; + }; + } + + // Activate Project Grant + // + // Set the state of the project grant to activated. + // + // Required permission: + // - `project.grant.write` + rpc ActivateProjectGrant(ActivateProjectGrantRequest) returns (ActivateProjectGrantResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/grants/{granted_organization_id}/activate" + 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: "Project grant activated successfully"; + }; + }; + }; + } + + // List Project Grants + // + // Returns a list of project grants. A project grant is when the organization grants its project to another organization. + // + // Required permission: + // - `project.grant.write` + rpc ListProjectGrants(ListProjectGrantsRequest) returns (ListProjectGrantsResponse) { + option (google.api.http) = { + post: "/v2beta/projects/grants/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.grant.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all project grants matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } +} + +message CreateProjectRequest { + // The unique identifier of the organization the project belongs to. + string organization_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\""; + } + ]; + // The unique identifier of the project. + optional 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\""; + } + ]; + // Name of the project. + string name = 3 [ + (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: "\"MyProject\""; + } + ]; + // Enable this setting to have role information included in the user info endpoint. It is also dependent on your application settings to include it in tokens and other types. + bool project_role_assertion = 4; + // When enabled ZITADEL will check if a user has an authorization to use this project assigned when login into an application of this project. + bool authorization_required = 5; + // When enabled ZITADEL will check if the organization of the user, that is trying to log in, has access to this project (either owns the project or is granted). + bool project_access_required = 6; + // Define which private labeling/branding should trigger when getting to a login of this project. + PrivateLabelingSetting private_labeling_setting = 7 [ + (validate.rules).enum = {defined_only: true} + ]; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"organizationId\":\"69629026806489455\",\"name\":\"MyProject\",\"projectRoleAssertion\":true,\"projectRoleCheck\":true,\"hasProjectCheck\":true,\"privateLabelingSetting\":\"PRIVATE_LABELING_SETTING_UNSPECIFIED\"}"; + }; +} + +message CreateProjectResponse { + // The unique identifier of the newly created project. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the project creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateProjectRequest { + 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\""; + } + ]; + // Name of the project. + optional string name = 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: "\"MyProject-Updated\""; + } + ]; + // Enable this setting to have role information included in the user info endpoint. It is also dependent on your application settings to include it in tokens and other types. + optional bool project_role_assertion = 3; + // When enabled ZITADEL will check if a user has a role of this project assigned when login into an application of this project. + optional bool project_role_check = 4; + // When enabled ZITADEL will check if the organization of the user, that is trying to log in, has a grant to this project. + optional bool has_project_check = 5; + // Define which private labeling/branding should trigger when getting to a login of this project. + optional PrivateLabelingSetting private_labeling_setting = 6 [ + (validate.rules).enum = {defined_only: true} + ]; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"name\":\"MyProject-Updated\",\"projectRoleAssertion\":true,\"projectRoleCheck\":true,\"hasProjectCheck\":true,\"privateLabelingSetting\":\"PRIVATE_LABELING_SETTING_UNSPECIFIED\"}"; + }; +} + +message UpdateProjectResponse { + // The timestamp of the change of the project. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteProjectRequest { + // The unique identifier of the project. + 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 DeleteProjectResponse { + // The timestamp of the deletion of the project. + // 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 GetProjectRequest { + // The unique identifier of the project. + 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 GetProjectResponse { + Project project = 1; +} + +message ListProjectsRequest { + // 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 ProjectFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PROJECT_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ProjectSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"PROJECT_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"projectNameFilter\":{\"projectName\":\"MyProject\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inProjectIdsFilter\":{\"projectIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}"; + }; +} + +message ListProjectsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated Project projects = 2; +} + +message DeactivateProjectRequest { + // The unique identifier of the project. + 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 DeactivateProjectResponse { + // The timestamp of the change of the project. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ActivateProjectRequest { + // The unique identifier of the project. + 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 ActivateProjectResponse { + // The timestamp of the change of the project. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message AddProjectRoleRequest { + // ID of the project. + string project_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\""; + } + ]; + // The key is the only relevant attribute for ZITADEL regarding the authorization checks. + string role_key = 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: "\"ADMIN\""; + } + ]; + // Name displayed for the role. + string display_name = 3 [ + (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: "\"Administrator\""; + } + ]; + // The group is only used for display purposes. That you have better handling, like giving all the roles from a group to a user. + optional string group = 4 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Admins\""; + } + ]; +} + +message AddProjectRoleResponse { + // The timestamp of the project role creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateProjectRoleRequest { + // ID of the project. + string project_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\""; + } + ]; + // The key is the only relevant attribute for ZITADEL regarding the authorization checks. + string role_key = 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: "\"ADMIN\""; + } + ]; + // Name displayed for the role. + optional string display_name = 3 [ + (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: "\"Administrator\""; + } + ]; + // The group is only used for display purposes. That you have better handling, like giving all the roles from a group to a user. + optional string group = 4 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Admins\""; + } + ]; +} + +message UpdateProjectRoleResponse { + // The timestamp of the change of the project role. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message RemoveProjectRoleRequest { + // ID of the project. + string project_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\""; + } + ]; + // The key is the only relevant attribute for ZITADEL regarding the authorization checks. + string role_key = 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: "\"ADMIN\""; + } + ]; +} + +message RemoveProjectRoleResponse { + // The timestamp of the removal of the project role. + // Note that the removal date is only guaranteed to be set if the removal was successful during the request. + // In case the removal occurred in a previous request, the removal date might be empty. + google.protobuf.Timestamp removal_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListProjectRolesRequest { + // ID of the project. + string project_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\""; + } + ]; + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 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 ProjectRoleFieldName sorting_column = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PROJECT_ROLE_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ProjectRoleSearchFilter filters = 4; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"PROJECT_ROLE_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"keyFilter\":{\"key\":\"role.super.man\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"displayNameFilter\":{\"displayName\":\"SUPER\"}}]}"; + }; +} + +message ListProjectRolesResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated ProjectRole project_roles = 2; +} + + +message CreateProjectGrantRequest { + // ID of the project. + string project_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\""; + } + ]; + // Organization the project is granted to. + string granted_organization_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: "\"28746028909593987\"" + } + ]; + // Keys of the role available for the project grant. + repeated string role_keys = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"RoleKey1\", \"RoleKey2\"]"; + } + ]; +} + +message CreateProjectGrantResponse { + // The timestamp of the project grant creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateProjectGrantRequest { + // ID of the project. + string project_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\""; + } + ]; + // Organization the project is granted to. + string granted_organization_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: "\"28746028909593987\"" + } + ]; + // Keys of the role available for the project grant. + repeated string role_keys = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"RoleKey1\", \"RoleKey2\"]"; + } + ]; +} + +message UpdateProjectGrantResponse { + // The timestamp of the change of the project grant. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteProjectGrantRequest { + // ID of the project. + string project_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\""; + } + ]; + // Organization the project is granted to. + string granted_organization_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: "\"28746028909593987\"" + } + ]; +} + +message DeleteProjectGrantResponse { + // The timestamp of the deletion of the project grant. + // 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 DeactivateProjectGrantRequest { + // ID of the project. + string project_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\""; + } + ]; + // Organization the project is granted to. + string granted_organization_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: "\"28746028909593987\"" + } + ]; +} + +message DeactivateProjectGrantResponse { + // The timestamp of the change of the project grant. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ActivateProjectGrantRequest { + // ID of the project. + string project_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\""; + } + ]; + // Organization the project is granted to. + string granted_organization_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: "\"28746028909593987\"" + } + ]; +} + +message ActivateProjectGrantResponse { + // The timestamp of the change of the project grant. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListProjectGrantsRequest { + // 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 ProjectGrantFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PROJECT_GRANT_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ProjectGrantSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"filters\":[{\"projectNameFilter\":{\"projectName\":\"MyProject\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inProjectIdsFilter\":{\"projectIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}"; + }; +} + +message ListProjectGrantsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated ProjectGrant project_grants = 2; +} \ No newline at end of file diff --git a/proto/zitadel/project/v2beta/query.proto b/proto/zitadel/project/v2beta/query.proto new file mode 100644 index 0000000000..9bfde662a3 --- /dev/null +++ b/proto/zitadel/project/v2beta/query.proto @@ -0,0 +1,297 @@ +syntax = "proto3"; + +package zitadel.project.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/project/v2beta;project"; + +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +import "zitadel/filter/v2beta/filter.proto"; + +message ProjectGrant { + // The unique identifier of the organization which granted the project to the granted_organization_id. + string organization_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the granted project creation. + google.protobuf.Timestamp creation_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the granted project (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // The ID of the organization the project is granted to. + string granted_organization_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The name of the organization the project is granted to. + string granted_organization_name = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Some Organization\"" + } + ]; + // The roles of the granted project. + repeated string granted_role_keys = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"role.super.man\"]" + } + ]; + // The ID of the granted project. + string project_id = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The name of the granted project. + string project_name = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\"" + } + ]; + // Describes the current state of the granted project. + ProjectGrantState state = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the project"; + } + ]; +} + +enum ProjectGrantState { + PROJECT_GRANT_STATE_UNSPECIFIED = 0; + PROJECT_GRANT_STATE_ACTIVE = 1; + PROJECT_GRANT_STATE_INACTIVE = 2; +} + +message Project { + // The unique identifier of the project. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the organization the project belongs to. + string organization_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the project creation. + google.protobuf.Timestamp creation_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the project (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // The name of the project. + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + } + ]; + // Describes the current state of the project. + ProjectState state = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the project"; + } + ]; + // Describes if the roles of the user should be added to the token. + bool project_role_assertion = 7; + // When enabled ZITADEL will check if a user has an authorization to use this project assigned when login into an application of this project. + bool authorization_required = 8; + // When enabled ZITADEL will check if the organization of the user, that is trying to log in, has access to this project (either owns the project or is granted). + bool project_access_required = 9; + // Defines from where the private labeling should be triggered. + PrivateLabelingSetting private_labeling_setting = 10; + + // The ID of the organization the project is granted to. + optional string granted_organization_id = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The name of the organization the project is granted to. + optional string granted_organization_name = 13 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Some Organization\"" + } + ]; + // Describes the current state of the granted project. + GrantedProjectState granted_state = 14 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the granted project"; + } + ]; +} + +enum ProjectState { + PROJECT_STATE_UNSPECIFIED = 0; + PROJECT_STATE_ACTIVE = 1; + PROJECT_STATE_INACTIVE = 2; +} + +enum GrantedProjectState { + GRANTED_PROJECT_STATE_UNSPECIFIED = 0; + GRANTED_PROJECT_STATE_ACTIVE = 1; + GRANTED_PROJECT_STATE_INACTIVE = 2; +} + +enum PrivateLabelingSetting { + PRIVATE_LABELING_SETTING_UNSPECIFIED = 0; + PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY = 1; + PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY = 2; +} + +enum ProjectFieldName { + PROJECT_FIELD_NAME_UNSPECIFIED = 0; + PROJECT_FIELD_NAME_ID = 1; + PROJECT_FIELD_NAME_CREATION_DATE = 2; + PROJECT_FIELD_NAME_CHANGE_DATE = 3; + PROJECT_FIELD_NAME_NAME = 4; +} + +enum ProjectGrantFieldName { + PROJECT_GRANT_FIELD_NAME_UNSPECIFIED = 0; + PROJECT_GRANT_FIELD_NAME_PROJECT_ID = 1; + PROJECT_GRANT_FIELD_NAME_CREATION_DATE = 2; + PROJECT_GRANT_FIELD_NAME_CHANGE_DATE = 3; +} + +enum ProjectRoleFieldName { + PROJECT_ROLE_FIELD_NAME_UNSPECIFIED = 0; + PROJECT_ROLE_FIELD_NAME_KEY = 1; + PROJECT_ROLE_FIELD_NAME_CREATION_DATE = 2; + PROJECT_ROLE_FIELD_NAME_CHANGE_DATE = 3; +} + +message ProjectSearchFilter { + oneof filter { + option (validate.required) = true; + + ProjectNameFilter project_name_filter = 1; + zitadel.filter.v2beta.InIDsFilter in_project_ids_filter = 2; + zitadel.filter.v2beta.IDFilter project_resource_owner_filter = 3; + zitadel.filter.v2beta.IDFilter project_grant_resource_owner_filter = 4; + zitadel.filter.v2beta.IDFilter project_organization_id_filter = 5; + } +} + +message ProjectNameFilter { + // Defines the name of the project to query for. + string project_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.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"; + } + ]; +} + +message ProjectGrantSearchFilter { + oneof filter { + option (validate.required) = true; + + ProjectNameFilter project_name_filter = 1; + ProjectRoleKeyFilter role_key_filter = 2; + zitadel.filter.v2beta.InIDsFilter in_project_ids_filter = 3; + zitadel.filter.v2beta.IDFilter project_resource_owner_filter = 4; + zitadel.filter.v2beta.IDFilter project_grant_resource_owner_filter = 5; + } +} + +message ProjectRole { + // ID of the project. + string project_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629026806489455\""; + } + ]; + // Key of the project role. + string key = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"role.super.man\"" + } + ]; + // The timestamp of the project role creation. + google.protobuf.Timestamp creation_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the project role. + google.protobuf.Timestamp change_date = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // Display name of the project role. + string display_name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Super man\"" + } + ]; + // Group of the project role. + string group = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"people\"" + } + ]; +} + +message ProjectRoleSearchFilter { + oneof filter { + option (validate.required) = true; + + ProjectRoleKeyFilter role_key_filter = 1; + ProjectRoleDisplayNameFilter display_name_filter = 2; + } +} + +message ProjectRoleKeyFilter { + string key = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"role.super.man\"" + } + ]; + // Defines which text comparison method used for the name query. + zitadel.filter.v2beta.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message ProjectRoleDisplayNameFilter { + string display_name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"SUPER\"" + } + ]; + // Defines which text comparison method used for the name query. + zitadel.filter.v2beta.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index f124c37a79..09b5559fb9 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -117,6 +117,8 @@ service SystemService { } // Returns a list of ZITADEL instances + // + // Deprecated: Use [ListInstances](apis/resources/instance_service_v2/instance-service-list-instances.api.mdx) instead to list instances rpc ListInstances(ListInstancesRequest) returns (ListInstancesResponse) { option (google.api.http) = { post: "/instances/_search" @@ -126,9 +128,15 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.read"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Returns the detail of an instance + // + // Deprecated: Use [GetInstance](apis/resources/instance_service_v2/instance-service-get-instance.api.mdx) instead to get the details of the instance in context rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse) { option (google.api.http) = { get: "/instances/{instance_id}"; @@ -137,6 +145,10 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.read"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Deprecated: Use CreateInstance instead @@ -151,9 +163,15 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.write"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Updates name of an existing instance + // + // Deprecated: Use [UpdateInstance](apis/resources/instance_service_v2/instance-service-update-instance.api.mdx) instead to update the name of the instance in context rpc UpdateInstance(UpdateInstanceRequest) returns (UpdateInstanceResponse) { option (google.api.http) = { put: "/instances/{instance_id}" @@ -163,6 +181,10 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.write"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Creates a new instance with all needed setup data @@ -180,6 +202,8 @@ service SystemService { // Removes an instance // This might take some time + // + // Deprecated: Use [DeleteInstance](apis/resources/instance_service_v2/instance-service-delete-instance.api.mdx) instead to delete an instance rpc RemoveInstance(RemoveInstanceRequest) returns (RemoveInstanceResponse) { option (google.api.http) = { delete: "/instances/{instance_id}" @@ -188,6 +212,10 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.delete"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } //Returns all instance members matching the request @@ -204,7 +232,9 @@ service SystemService { }; } - //Checks if a domain exists + // Checks if a domain exists + // + // Deprecated: Use [ListCustomDomains](apis/resources/instance_service_v2/instance-service-list-custom-domains.api.mdx) instead to check existence of an instance rpc ExistsDomain(ExistsDomainRequest) returns (ExistsDomainResponse) { option (google.api.http) = { post: "/domains/{domain}/_exists"; @@ -214,10 +244,14 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.domain.read"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Returns the custom domains of an instance - //Checks if a domain exists + // Checks if a domain exists // Deprecated: Use the Admin APIs ListInstanceDomains on the admin API instead rpc ListDomains(ListDomainsRequest) returns (ListDomainsResponse) { option (google.api.http) = { @@ -228,9 +262,15 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.domain.read"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Adds a domain to an instance + // + // Deprecated: Use [AddCustomDomain](apis/resources/instance_service_v2/instance-service-add-custom-domain.api.mdx) instead to add a custom domain to the instance in context rpc AddDomain(AddDomainRequest) returns (AddDomainResponse) { option (google.api.http) = { post: "/instances/{instance_id}/domains"; @@ -240,9 +280,15 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.domain.write"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Removes the domain of an instance + // + // Deprecated: Use [RemoveDomain](apis/resources/instance_service_v2/instance-service-remove-custom-domain.api.mdx) instead to remove a custom domain from the instance in context rpc RemoveDomain(RemoveDomainRequest) returns (RemoveDomainResponse) { option (google.api.http) = { delete: "/instances/{instance_id}/domains/{domain}"; @@ -251,6 +297,10 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.domain.delete"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Sets the primary domain of an instance diff --git a/proto/zitadel/user/v2/email.proto b/proto/zitadel/user/v2/email.proto index e962707fcf..eb807206a7 100644 --- a/proto/zitadel/user/v2/email.proto +++ b/proto/zitadel/user/v2/email.proto @@ -19,7 +19,7 @@ message SetHumanEmail { example: "\"mini@mouse.com\""; } ]; - // if no verification is specified, an email is sent with the default url + // If no verification is specified, an email is sent with the default url oneof verification { SendEmailVerificationCode send_code = 2; ReturnEmailVerificationCode return_code = 3; diff --git a/proto/zitadel/user/v2/key.proto b/proto/zitadel/user/v2/key.proto new file mode 100644 index 0000000000..ffa83c714e --- /dev/null +++ b/proto/zitadel/user/v2/key.proto @@ -0,0 +1,69 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; + +message Key { + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change of the key. + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The unique identifier of the key. + string id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the user the key belongs to. + string user_id = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the organization the key belongs to. + string organization_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The keys expiration date. + google.protobuf.Timestamp expiration_date = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message KeysSearchFilter { + oneof filter { + option (validate.required) = true; + zitadel.filter.v2.IDFilter key_id_filter = 1; + zitadel.filter.v2.IDFilter user_id_filter = 2; + zitadel.filter.v2.IDFilter organization_id_filter = 3; + zitadel.filter.v2.TimestampFilter created_date_filter = 4; + zitadel.filter.v2.TimestampFilter expiration_date_filter = 5; + } +} + +enum KeyFieldName { + KEY_FIELD_NAME_UNSPECIFIED = 0; + KEY_FIELD_NAME_CREATED_DATE = 1; + KEY_FIELD_NAME_ID = 2; + KEY_FIELD_NAME_USER_ID = 3; + KEY_FIELD_NAME_ORGANIZATION_ID = 4; + KEY_FIELD_NAME_KEY_EXPIRATION_DATE = 5; +} diff --git a/proto/zitadel/user/v2/pat.proto b/proto/zitadel/user/v2/pat.proto new file mode 100644 index 0000000000..1d24c4c496 --- /dev/null +++ b/proto/zitadel/user/v2/pat.proto @@ -0,0 +1,70 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; + +message PersonalAccessToken { + // The timestamp of the personal access token creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change of the personal access token. + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The unique identifier of the personal access token. + string id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the user the personal access token belongs to. + string user_id = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the organization the personal access token belongs to. + string organization_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The personal access tokens expiration date. + google.protobuf.Timestamp expiration_date = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message PersonalAccessTokensSearchFilter { + oneof filter { + option (validate.required) = true; + zitadel.filter.v2.IDFilter token_id_filter = 1; + zitadel.filter.v2.IDFilter user_id_filter = 2; + zitadel.filter.v2.IDFilter organization_id_filter = 3; + zitadel.filter.v2.TimestampFilter created_date_filter = 4; + zitadel.filter.v2.TimestampFilter expiration_date_filter = 5; + } +} + +enum PersonalAccessTokenFieldName { + PERSONAL_ACCESS_TOKEN_FIELD_NAME_UNSPECIFIED = 0; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE = 1; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID = 2; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_USER_ID = 3; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_ORGANIZATION_ID = 4; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE = 5; +} + diff --git a/proto/zitadel/user/v2/user.proto b/proto/zitadel/user/v2/user.proto index e2a140ea27..9ea2b8906e 100644 --- a/proto/zitadel/user/v2/user.proto +++ b/proto/zitadel/user/v2/user.proto @@ -334,7 +334,7 @@ message AuthFactorU2F { message SendInviteCode { // Optionally set a url_template, which will be used in the invite mail sent by ZITADEL to guide the user to your invitation page. - // If no template is set, the default ZITADEL url will be used. + // If no template is set and no previous code was created, the default ZITADEL url will be used. // // The following placeholders can be used: UserID, OrgID, Code optional string url_template = 1 [ @@ -346,7 +346,7 @@ message SendInviteCode { } ]; // Optionally set an application name, which will be used in the invite mail sent by ZITADEL. - // If no application name is set, ZITADEL will be used as default. + // If no application name is set and no previous code was created, ZITADEL will be used as default. optional string application_name = 2 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 00cb352f70..3fc81836d6 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -2,6 +2,14 @@ syntax = "proto3"; package zitadel.user.v2; +import "google/protobuf/timestamp.proto"; +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/object/v2/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; import "zitadel/user/v2/auth.proto"; @@ -10,13 +18,10 @@ import "zitadel/user/v2/phone.proto"; import "zitadel/user/v2/idp.proto"; import "zitadel/user/v2/password.proto"; import "zitadel/user/v2/user.proto"; +import "zitadel/user/v2/key.proto"; +import "zitadel/user/v2/pat.proto"; import "zitadel/user/v2/query.proto"; -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/filter/v2/filter.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; @@ -85,9 +90,9 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } } responses: { - key: "403"; + key: "400"; value: { - description: "Returned when the user does not have permission to access the resource."; + description: "The request is malformed."; schema: { json_schema: { ref: "#/definitions/rpcStatus"; @@ -96,9 +101,20 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } } responses: { - key: "404"; + key: "401"; value: { - description: "Returned when the resource does not exist."; + description: "Returned when the user is not authenticated."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; schema: { json_schema: { ref: "#/definitions/rpcStatus"; @@ -110,8 +126,51 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { service UserService { + // Create a User + // + // Create a new human or machine user in the specified organization. + // + // Required permission: + // - user.write + rpc CreateUser (CreateUserRequest) returns (CreateUserResponse) { + option (google.api.http) = { + // The /new path segment does not follow Zitadels API design. + // The only reason why it is used here is to avoid a conflict with the ListUsers endpoint, which already handles POST /v2/users. + post: "/v2/users/new" + 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"; + } + }; + responses: { + key: "409" + value: { + description: "The user already exists."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + // Create a new human user // + // Deprecated: Use [CreateUser](apis/resources/user_service_v2/user-service-create-user.api.mdx) to create a new user of type human instead. + // // Create/import a new user with the type human. The newly created user will get a verification email if either the email address is not marked as verified and you did not request the verification to be returned. rpc AddHumanUser (AddHumanUserRequest) returns (AddHumanUserResponse) { option (google.api.http) = { @@ -125,11 +184,12 @@ service UserService { org_field: "organization" } http_response: { - success_code: 201 + success_code: 200 } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { @@ -163,6 +223,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -204,6 +270,8 @@ service UserService { // Change the user email // + // Deprecated: [Update the users email field](apis/resources/user_service_v2/user-service-update-user.api.mdx). + // // Change the email address of a user. If the state is set to not verified, a verification code will be generated, which can be either returned or sent to the user by email.. rpc SetEmail (SetEmailRequest) returns (SetEmailResponse) { option (google.api.http) = { @@ -218,18 +286,23 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Resend code to verify user email - // - // Resend code to verify user email. rpc ResendEmailCode (ResendEmailCodeRequest) returns (ResendEmailCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/email/resend" @@ -249,12 +322,16 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Send code to verify user email - // - // Send code to verify user email. rpc SendEmailCode (SendEmailCodeRequest) returns (SendEmailCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/email/send" @@ -274,6 +351,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -299,11 +382,19 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Set the user phone // + // Deprecated: [Update the users phone field](apis/resources/user_service_v2/user-service-update-user.api.mdx). + // // Set the phone number of a user. If the state is set to not verified, a verification code will be generated, which can be either returned or sent to the user by sms.. rpc SetPhone(SetPhoneRequest) returns (SetPhoneResponse) { option (google.api.http) = { @@ -318,18 +409,27 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } - // Remove the user phone + // Delete the user phone // - // Remove the user phone + // Deprecated: [Update the users phone field](apis/resources/user_service_v2/user-service-update-user.api.mdx) to remove the phone number. + // + // Delete the phone number of a user. rpc RemovePhone(RemovePhoneRequest) returns (RemovePhoneResponse) { option (google.api.http) = { delete: "/v2/users/{user_id}/phone" @@ -343,20 +443,23 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete the user phone"; - description: "Delete the phone number of a user." + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Resend code to verify user phone - // - // Resend code to verify user phone. rpc ResendPhoneCode (ResendPhoneCodeRequest) returns (ResendPhoneCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/phone/resend" @@ -376,6 +479,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -401,15 +510,27 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } - // Update User + + // Update a User // - // Update all information from a user.. - rpc UpdateHumanUser(UpdateHumanUserRequest) returns (UpdateHumanUserResponse) { + // Partially update an existing user. + // If you change the users email or phone, you can specify how the ownership should be verified. + // If you change the users password, you can specify if the password should be changed again on the users next login. + // + // Required permission: + // - user.write + rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse) { option (google.api.http) = { - put: "/v2/users/human/{user_id}" + patch: "/v2/users/{user_id}" body: "*" }; @@ -426,6 +547,62 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + } + responses: { + key: "409" + value: { + description: "The user already exists."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Update Human User + // + // Deprecated: Use [UpdateUser](apis/resources/user_service_v2/user-service-update-user.api.mdx) to update a user of type human instead. + // + // Update all information from a user.. + rpc UpdateHumanUser(UpdateHumanUserRequest) returns (UpdateHumanUserResponse) { + option (google.api.http) = { + put: "/v2/users/human/{user_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + responses: { + key: "200" + value: { + description: "OK"; + } + }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -451,6 +628,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -476,6 +659,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -501,6 +690,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -526,6 +721,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -539,7 +740,7 @@ service UserService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "user.delete" + permission: "authenticated" } }; @@ -550,6 +751,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -574,6 +781,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -598,6 +811,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -622,6 +841,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -670,6 +895,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -694,6 +925,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -718,6 +955,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -743,6 +986,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -767,6 +1016,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -791,6 +1046,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -814,6 +1075,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -838,6 +1105,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -861,6 +1134,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -885,6 +1164,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -908,6 +1193,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -958,6 +1249,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "Intent ID does not exist."; + } + } }; } @@ -983,6 +1280,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1033,6 +1336,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1058,11 +1367,19 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Change password // + // Deprecated: [Update the users password](apis/resources/user_service_v2/user-service-update-user.api.mdx) instead. + // // Change the password of a user with either a verification code or the current password.. rpc SetPassword (SetPasswordRequest) returns (SetPasswordResponse) { option (google.api.http) = { @@ -1077,12 +1394,19 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1135,6 +1459,8 @@ service UserService { // Create an invite code for a user // // Create an invite code for a user to initialize their first authentication method (password, passkeys, IdP) depending on the organization's available methods. + // If an invite code has been created previously, it's url template and application name will be used as defaults for the new code. + // The new code will overwrite the previous one and make it invalid. rpc CreateInviteCode (CreateInviteCodeRequest) returns (CreateInviteCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/invite_code" @@ -1153,11 +1479,19 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Resend an invite code for a user // + // Deprecated: Use [CreateInviteCode](apis/resources/user_service_v2/user-service-create-invite-code.api.mdx) instead. + // // Resend an invite code for a user to initialize their first authentication method (password, passkeys, IdP) depending on the organization's available methods. // A resend is only possible if a code has been created previously and sent to the user. If there is no code or it was directly returned, an error will be returned. rpc ResendInviteCode (ResendInviteCodeRequest) returns (ResendInviteCodeResponse) { @@ -1172,12 +1506,19 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1204,6 +1545,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1229,9 +1576,289 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } + // Add a Users Secret + // + // Generates a client secret for the user. + // The client id is the users username. + // If the user already has a secret, it is overwritten. + // Only users of type machine can have a secret. + // + // Required permission: + // - user.write + rpc AddSecret(AddSecretRequest) returns (AddSecretResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/secret" + 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: "The secret was successfully generated."; + } + }; + responses: { + key: "404" + value: { + description: "The user ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Remove a Users Secret + // + // Remove the current client ID and client secret from a machine user. + // + // Required permission: + // - user.write + rpc RemoveSecret(RemoveSecretRequest) returns (RemoveSecretResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/secret" + }; + + 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: "The secret was either successfully removed or it didn't exist in the first place."; + } + }; + }; + } + + // Add a Key + // + // Add a keys that can be used to securely authenticate at the Zitadel APIs using JWT profile authentication using short-lived tokens. + // Make sure you store the returned key safely, as you won't be able to read it from the Zitadel API anymore. + // Only users of type machine can have keys. + // + // Required permission: + // - user.write + rpc AddKey(AddKeyRequest) returns (AddKeyResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/keys" + 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: "The key was successfully created."; + } + }; + responses: { + key: "404" + value: { + description: "The user ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Remove a Key + // + // Remove a machine users key by the given key ID and an optionally given user ID. + // + // Required permission: + // - user.write + rpc RemoveKey(RemoveKeyRequest) returns (RemoveKeyResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/keys/{key_id}" + }; + + 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: "The key was either successfully removed or it not found in the first place."; + } + }; + }; + } + + // Search Keys + // + // List all matching keys. By default all keys of the instance on which the caller has permission to read the owning users are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - user.read + rpc ListKeys(ListKeysRequest) returns (ListKeysResponse) { + option (google.api.http) = { + post: "/v2/users/keys/search" + 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: "A list of all machine user keys matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // Add a Personal Access Token + // + // Personal access tokens (PAT) are the easiest way to authenticate to the Zitadel APIs. + // Make sure you store the returned PAT safely, as you won't be able to read it from the Zitadel API anymore. + // Only users of type machine can have personal access tokens. + // + // Required permission: + // - user.write + rpc AddPersonalAccessToken(AddPersonalAccessTokenRequest) returns (AddPersonalAccessTokenResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/pats" + 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: "The personal access token was successfully created."; + } + }; + responses: { + key: "404" + value: { + description: "The user ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Remove a Personal Access Token + // + // Removes a machine users personal access token by the given token ID and an optionally given user ID. + // + // Required permission: + // - user.write + rpc RemovePersonalAccessToken(RemovePersonalAccessTokenRequest) returns (RemovePersonalAccessTokenResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/pats/{token_id}" + }; + + 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: "The personal access token was either successfully removed or it was not found in the first place."; + } + }; + }; + } + + // Search Personal Access Tokens + // + // List all personal access tokens. By default all personal access tokens of the instance on which the caller has permission to read the owning users are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - user.read + rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) { + option (google.api.http) = { + post: "/v2/users/pats/search" + 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: "A list of all personal access tokens matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } } message AddHumanUserRequest{ @@ -1291,6 +1918,149 @@ message AddHumanUserResponse { optional string phone_code = 4; } + +message CreateUserRequest{ + message Human { + // Set the users profile information. + SetHumanProfile profile = 1 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED + ]; + // Set the users email address and optionally send a verification email. + SetHumanEmail email = 2 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED + ]; + // Set the users phone number and optionally send a verification SMS. + optional SetHumanPhone phone = 3; + // Set the users initial password and optionally require the user to set a new password. + oneof password_type { + Password password = 4; + HashedPassword hashed_password = 5; + } + // Create the user with a list of links to identity providers. + // This can be useful in migration-scenarios. + // For example, if a user already has an account in an external identity provider or another Zitadel instance, an IDP link allows the user to authenticate as usual. + // Sessions, second factors, hardware keys registered externally are still available for authentication. + // Use the following endpoints to manage identity provider links: + // - [AddIDPLink](apis/resources/user_service_v2/user-service-add-idp-link.api.mdx) + // - [RemoveIDPLink](apis/resources/user_service_v2/user-service-remove-idp-link.api.mdx) + repeated IDPLink idp_links = 7; + // An Implementation of RFC 6238 is used, with HMAC-SHA-1 and time-step of 30 seconds. + // Currently no other options are supported, and if anything different is used the validation will fail. + optional string totp_secret = 8 [ + (validate.rules).string = {min_len: 1 max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\""; + } + ]; + } + message Machine { + // The machine users name is a human readable field that helps identifying the user. + string name = 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: "\"Acceptance Test User\""; + } + ]; + // The description is a field that helps to remember the purpose of the user. + optional string description = 2 [ + (validate.rules).string = {min_len: 1, max_len: 500}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 500, + example: "\"The user calls the session API in the continuous integration pipeline for acceptance tests.\""; + } + ]; + } + // The unique identifier of the organization the user belongs to. + string organization_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\""; + } + ]; + // The ID is a unique identifier for the user in the instance. + // If not specified, it will be generated. + // You can set your own user id that is unique within the instance. + // This is useful in migration scenarios, for example if the user already has an ID in another Zitadel system. + // If not specified, it will be generated. + // It can't be changed after creation. + optional string user_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: "\"163840776835432345\""; + } + ]; + + // The username is a unique identifier for the user in the organization. + // If not specified, Zitadel sets the username to the email for users of type human and to the user_id for users of type machine. + // It is used to identify the user in the organization and can be used for login. + optional string username = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"minnie-mouse\""; + } + ]; + + // The type of the user. + oneof user_type { + option (validate.required) = true; + // Users of type human are users that are meant to be used by a person. + // They can log in interactively using a login UI. + // By default, new users will receive a verification email and, if a phone is configured, a verification SMS. + // To make sure these messages are sent, configure and activate valid SMTP and Twilio configurations. + // Read more about your options for controlling this behaviour in the email and phone field documentations. + Human human = 4; + // Users of type machine are users that are meant to be used by a machine. + // In order to authenticate, [add a secret](apis/resources/user_service_v2/user-service-add-secret.api.mdx), [a key](apis/resources/user_service_v2/user-service-add-key.api.mdx) or [a personal access token](apis/resources/user_service_v2/user-service-add-personal-access-token.api.mdx) to the user. + // Tokens generated for new users of type machine will be of an opaque Bearer type. + // You can change the users token type to JWT by using the [management v1 service method UpdateMachine](apis/resources/mgmt/management-service-update-machine.api.mdx). + Machine machine = 5; + } + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"organizationId\":\"69629026806489455\",\"userId\":\"163840776835432345\",\"username\":\"minnie-mouse\",\"human\":{\"profile\":{\"givenName\":\"Minnie\",\"familyName\":\"Mouse\",\"nickName\":\"Mini\",\"displayName\":\"Minnie Mouse\",\"preferredLanguage\":\"en\",\"gender\":\"GENDER_FEMALE\"},\"email\":{\"email\":\"mini@mouse.com\",\"sendCode\":{\"urlTemplate\":\"https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\"}},\"phone\":{\"phone\":\"+41791234567\",\"isVerified\":true},\"password\":{\"password\":\"Secr3tP4ssw0rd!\",\"changeRequired\":true},\"idpLinks\":[{\"idpId\":\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\",\"userId\":\"6516849804890468048461403518\",\"userName\":\"user@external.com\"}],\"totpSecret\":\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\"}}"; + }; +} + +message CreateUserResponse { + // The unique identifier of the newly created user. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the user creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // The email verification code if it was requested by setting the email verification to return_code. + optional string email_code = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"XTA6BC\""; + } + ]; + // The phone verification code if it was requested by setting the phone verification to return_code. + optional string phone_code = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"XTA6BC\""; + } + ]; +} + message GetUserByIDRequest { reserved 2; reserved "organization"; @@ -1545,6 +2315,142 @@ message DeleteUserResponse { zitadel.object.v2.Details details = 1; } + +message UpdateUserRequest{ + message Human { + message Profile { + // The given name is the first name of the user. + // For example, it can be used to personalize notifications and login UIs. + optional string given_name = 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: "\"Minnie\""; + } + ]; + // The family name is the last name of the user. + // For example, it can be used to personalize user interfaces and notifications. + optional string family_name = 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: "\"Mouse\""; + } + ]; + // The nick name is the users short name. + // For example, it can be used to personalize user interfaces and notifications. + optional string nick_name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Mini\""; + } + ]; + // The display name is how a user should primarily be displayed in lists. + // It can also for example be used to personalize user interfaces and notifications. + optional string display_name = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Minnie Mouse\""; + } + ]; + // The users preferred language is the language that systems should use to interact with the user. + // It has the format of a [BCP-47 language tag](https://datatracker.ietf.org/doc/html/rfc3066). + // It is used by Zitadel where no higher prioritized preferred language can be used. + // For example, browser settings can overwrite a users preferred_language. + // Notification messages and standard login UIs use the users preferred language if it is supported and allowed on the instance. + // Else, the default language of the instance is used. + optional string preferred_language = 5 [ + (validate.rules).string = {min_len: 1, max_len: 10}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 10; + example: "\"en-US\""; + } + ]; + // The users gender can for example be used to personalize user interfaces and notifications. + optional Gender gender = 6 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"GENDER_FEMALE\""; + } + ]; + } + // Change the users profile information + optional Profile profile = 1; + // Change the users email address and/or trigger a verification email + optional SetHumanEmail email = 2; + // Change the users phone number and/or trigger a verification SMS + // To delete the users phone number, leave the phone field empty and omit the verification field. + optional SetHumanPhone phone = 3; + // Change the users password. + // You can optionally require the current password or the verification code to be correct. + optional SetPassword password = 4; + } + message Machine { + // The machine users name is a human readable field that helps identifying the user. + optional string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Acceptance Test User\""; + } + ]; + // The description is a field that helps to remember the purpose of the user. + optional string description = 2 [ + (validate.rules).string = {max_len: 500}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"The user calls the session API in the continuous integration pipeline for acceptance tests.\""; + } + ]; + } + // The user id is the users unique identifier in the instance. + // It can't be changed. + string user_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: "\"163840776835432345\""; + } + ]; + // Set a new username that is unique within the instance. + // Beware that active tokens and sessions are invalidated when the username is changed. + optional string username = 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: "\"minnie-mouse\""; + } + ]; + // Change type specific properties of the user. + oneof user_type { + Human human = 3; + Machine machine = 4; + } + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"username\":\"minnie-mouse\",\"human\":{\"profile\":{\"givenName\":\"Minnie\",\"familyName\":\"Mouse\",\"displayName\":\"Minnie Mouse\"},\"email\":{\"email\":\"mini@mouse.com\",\"returnCode\":{}},\"phone\":{\"phone\":\"+41791234567\",\"isVerified\":true},\"password\":{\"password\":{\"password\":\"Secr3tP4ssw0rd!\",\"changeRequired\":true},\"verificationCode\":\"SKJd342k\"},\"totpSecret\":\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\"}}"; + }; +} + +message UpdateUserResponse { + // The timestamp of the change of the user. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // In case the email verification was set to return_code, the code will be returned + optional string email_code = 2; + // In case the phone verification was set to return_code, the code will be returned + optional string phone_code = 3; +} + message UpdateHumanUserRequest{ string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -1590,7 +2496,6 @@ message DeactivateUserResponse { zitadel.object.v2.Details details = 1; } - message ReactivateUserRequest { string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -2379,3 +3284,237 @@ message HumanMFAInitSkippedRequest { message HumanMFAInitSkippedResponse { zitadel.object.v2.Details details = 1; } + +message AddSecretRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; +} + +message AddSecretResponse { + // The timestamp of the secret creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The client secret. + // Store this secret in a secure place. + // It is not possible to retrieve it again. + string client_secret = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"WoYLHB23HAZaCSxeMJGEzbu8urHICVdFp2IegVr6Q5U4lZHKAtRvmaalNDWfCuHV\""; + } + ]; +} + +message RemoveSecretRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; +} + +message RemoveSecretResponse { + // The timestamp of the secret deletion. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message AddKeyRequest { + // The users resource ID. + string user_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: "\"163840776835432345\""; + } + ]; + // The date the key will expire and no logins will be possible anymore. + google.protobuf.Timestamp expiration_date = 2 [ + (validate.rules).timestamp.required = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2519-04-01T08:45:00.000000Z\""; + } + ]; + // Optionally provide a public key of your own generated RSA private key. + bytes public_key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1...\""; + } + ]; +} + +message AddKeyResponse { + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The keys ID. + string key_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; + // The key which is usable to authenticate against the API. + bytes key_content = 3; +} + + +message RemoveKeyRequest { + // The users resource ID. + string user_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: "\"163840776835432345\""; + } + ]; + // The keys ID. + string key_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: "\"163840776835432345\""; + } + ]; +} + +message RemoveKeyResponse { + // The timestamp of the key deletion. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ListKeysRequest { + // List limitations and ordering. + optional zitadel.filter.v2.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 KeyFieldName sorting_column = 2 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"KEY_FIELD_NAME_CREATED_DATE\"" + } + ]; + // Define the criteria to query for. + repeated KeysSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":10,\"asc\":false},\"sortingColumn\":\"KEY_FIELD_NAME_CREATED_DATE\",\"filters\":[{\"andFilter\":{\"filters\":[{\"organizationIdFilter\":{\"organizationId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"notFilter\":{\"filter\":{\"userIdFilter\":{\"userId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}}},{\"orFilter\":{\"filters\":[{\"keyIdFilter\":{\"keyId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"keyIdFilter\":{\"keyId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}]}}]}}]}"; + }; +} + +message ListKeysResponse { + zitadel.filter.v2.PaginationResponse pagination = 1; + repeated Key result = 2; +} + +message AddPersonalAccessTokenRequest { + // The users resource ID. + string user_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: "\"163840776835432345\""; + } + ]; + // The timestamp when the token will expire. + google.protobuf.Timestamp expiration_date = 2 [ + (validate.rules).timestamp.required = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2519-04-01T08:45:00.000000Z\""; + } + ]; +} + +message AddPersonalAccessTokenResponse { + // The timestamp of the personal access token creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The tokens ID. + string token_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; + // The personal access token that can be used to authenticate against the API + string token = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c\""; + } + ]; +} + +message RemovePersonalAccessTokenRequest { + // The users resource ID. + string user_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: "\"163840776835432345\""; + } + ]; + // The tokens ID. + string token_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: "\"163840776835432345\""; + } + ]; +} + +message RemovePersonalAccessTokenResponse { + // The timestamp of the personal access token deletion. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + + +message ListPersonalAccessTokensRequest { + // List limitations and ordering. + optional zitadel.filter.v2.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 PersonalAccessTokenFieldName sorting_column = 2 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE\"" + } + ]; + // Define the criteria to query for. + repeated PersonalAccessTokensSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":10,\"asc\":false},\"sortingColumn\":\"PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE\",\"filters\":[{\"andFilter\":{\"filters\":[{\"organizationIdFilter\":{\"organizationId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"notFilter\":{\"filter\":{\"userIdFilter\":{\"userId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}}},{\"orFilter\":{\"filters\":[{\"personalAccessTokenIdFilter\":{\"personalAccessTokenId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"personalAccessTokenIdFilter\":{\"personalAccessTokenId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}]}}]}}]}"; + }; +} + +message ListPersonalAccessTokensResponse { + zitadel.filter.v2.PaginationResponse pagination = 1; + repeated PersonalAccessToken result = 2; +} diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto index 03bc36220e..f877252f51 100644 --- a/proto/zitadel/user/v2beta/user_service.proto +++ b/proto/zitadel/user/v2beta/user_service.proto @@ -563,7 +563,7 @@ service UserService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "user.delete" + permission: "authenticated" } }; diff --git a/proto/zitadel/v1.proto b/proto/zitadel/v1.proto index c186ea7d61..beb91116f1 100644 --- a/proto/zitadel/v1.proto +++ b/proto/zitadel/v1.proto @@ -172,10 +172,12 @@ message DataOIDCApplication { message DataHumanUser { string user_id = 1; zitadel.management.v1.ImportHumanUserRequest user = 2; + zitadel.user.v1.UserState state = 3; } message DataMachineUser { string user_id = 1; zitadel.management.v1.AddMachineUserRequest user = 2; + zitadel.user.v1.UserState state = 3; } message DataAction { string action_id = 1;