diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml index fb15beb726..a975c0b995 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml @@ -40,7 +40,7 @@ body: label: Database description: What database are you using? (self-hosters only) options: - - CockroachDB + - CockroachDB (Zitadel v2) - PostgreSQL - Other (describe below!) - type: input diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8a2f567304..b7354f3f4a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,17 @@ version: 2 updates: - package-ecosystem: npm - directory: "/console" + groups: + console: + applies-to: version-updates + patterns: + - "*" + update-types: + - "minor" + - "patch" + directories: + - "/console" + - "/e2e" schedule: interval: weekly time: "02:00" @@ -13,6 +23,14 @@ updates: prefix: chore include: scope - package-ecosystem: gomod + groups: + go: + applies-to: version-updates + patterns: + - "*" + update-types: + - "minor" + - "patch" directory: "/" schedule: interval: weekly @@ -25,6 +43,14 @@ updates: prefix: chore include: scope - package-ecosystem: "docker" + groups: + docker: + applies-to: version-updates + patterns: + - "*" + update-types: + - "minor" + - "patch" directory: "/build" schedule: interval: "weekly" @@ -34,6 +60,11 @@ updates: prefix: chore include: scope - package-ecosystem: "github-actions" + groups: + actions: + applies-to: version-updates + patterns: + - "*" directory: "/" schedule: interval: weekly diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 265902feff..979911d5ab 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,8 @@ + + # Which Problems Are Solved Replace this example text with a concise list of problems that this PR solves. diff --git a/.github/workflows/core-integration-test.yml b/.github/workflows/core-integration-test.yml index d889d7a5ff..e33ffec3d5 100644 --- a/.github/workflows/core-integration-test.yml +++ b/.github/workflows/core-integration-test.yml @@ -73,7 +73,6 @@ jobs: if: ${{ steps.cache.outputs.cache-hit != 'true' }} env: ZITADEL_MASTERKEY: MasterkeyNeedsToHave32Characters - INTEGRATION_DB_FLAVOR: postgres run: make core_integration_test - name: upload server logs @@ -99,71 +98,3 @@ jobs: with: key: integration-test-postgres-${{ inputs.core_cache_key }} path: ${{ steps.go-cache-path.outputs.GO_CACHE_PATH }} - - # TODO: produces the following output: ERROR: unknown command "cockroach start-single-node --insecure" for "cockroach" - # cockroach: - # runs-on: ubuntu-latest - # services: - # cockroach: - # image: cockroachdb/cockroach:latest - # ports: - # - 26257:26257 - # - 8080:8080 - # env: - # COCKROACH_ARGS: "start-single-node --insecure" - # options: >- - # --health-cmd "curl http://localhost:8080/health?ready=1 || exit 1" - # --health-interval 10s - # --health-timeout 5s - # --health-retries 5 - # --health-start-period 10s - # steps: - # - - # uses: actions/checkout@v4 - # - - # uses: actions/setup-go@v5 - # with: - # go-version: ${{ inputs.go_version }} - # - - # uses: actions/cache/restore@v4 - # timeout-minutes: 1 - # name: restore core - # with: - # path: ${{ inputs.core_cache_path }} - # key: ${{ inputs.core_cache_key }} - # fail-on-cache-miss: true - # - - # id: go-cache-path - # name: set cache path - # run: echo "GO_CACHE_PATH=$(go env GOCACHE)" >> $GITHUB_OUTPUT - # - - # uses: actions/cache/restore@v4 - # id: cache - # timeout-minutes: 1 - # name: restore previous results - # with: - # key: integration-test-crdb-${{ inputs.core_cache_key }} - # restore-keys: | - # integration-test-crdb-core- - # path: ${{ steps.go-cache-path.outputs.GO_CACHE_PATH }} - # - - # name: test - # if: ${{ steps.cache.outputs.cache-hit != 'true' }} - # env: - # ZITADEL_MASTERKEY: MasterkeyNeedsToHave32Characters - # INTEGRATION_DB_FLAVOR: cockroach - # run: make core_integration_test - # - - # name: publish coverage - # uses: codecov/codecov-action@v4.3.0 - # with: - # file: profile.cov - # name: core-integration-tests-cockroach - # flags: core-integration-tests-cockroach - # - - # uses: actions/cache/save@v4 - # name: cache results - # if: ${{ steps.cache.outputs.cache-hit != 'true' }} - # with: - # key: integration-test-crdb-${{ inputs.core_cache_key }} - # path: ${{ steps.go-cache-path.outputs.GO_CACHE_PATH }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 17aee6bbe9..23469d4209 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,6 @@ load-test/.keys # dumps .backups -cockroach-data/* .local/* .build/ @@ -71,7 +70,6 @@ zitadel-*-* # local build/local/*.env -migrations/cockroach/migrate_cloud.go .notifications /.artifacts/* !/.artifacts/zitadel diff --git a/API_DESIGN.md b/API_DESIGN.md index 3768c112aa..7df13d6588 100644 --- a/API_DESIGN.md +++ b/API_DESIGN.md @@ -158,9 +158,33 @@ 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 are organization specific and might only be checked based on the queried resource. -Therefore, the API does not check the permissions itself but relies on the checks of the functions that are called by the API. -Required permissions need to be documented in the [API documentation](#documentation). +Permissions grated to the user might be organization specific and can therefore only be checked based on the queried resource. +In such case, the API does not check the permissions itself but relies on the checks of the functions that are called by the API. +If the permission can be checked by the API itself, e.g. if the permission is instance wide, it can be annotated on the endpoint in the proto file (see below). +In any case, the required permissions need to be documented in the [API documentation](#documentation). + +### Permission annotations + +Permissions can be annotated on the endpoint in the proto file. This allows the API to automatically check the permissions for the user. +The permissions are checked by the middleware and an error is returned if the user does not have the required permissions. + +The following example requires the user to have the `iam.web_key.write` permission to call the `CreateWebKey` method. +```protobuf + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.write" + } +}; +``` + +In case the permission cannot be checked by the API itself, but all requests need to be from an authenticated user, the `auth_option` can be set to `authenticated`. +```protobuf + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } +}; +``` ## Pagination @@ -186,6 +210,8 @@ message ListQuery { On the corresponding responses the `ListDetails` can be used to return the total count of the resources and allow the user to handle their offset and limit accordingly. +The API MUST enforce a reasonable maximum limit for the number of resources that can be retrieved and returned in a single request. +The default limit is set to 100 and the maximum limit is set to 1000. If the client requests a limit that exceeds the maximum limit, an error is returned. ## Error Handling @@ -222,19 +248,21 @@ HTTP/1.1 400 Bad Request Content-Type: application/json { - "code": "user_missing_information", - "message": "missing required information for the creation of the user", + "code": "user_invalid_information", + "message": "invalid or missing information provided for the creation of the user", "details": [ { "@type": "type.googleapis.com/google.rpc.BadRequest", "fieldViolations": [ { "field": "given_name", - "description": "given name is required" + "description": "given name is required", + "reason": "MISSING_VALUE" }, { "field": "family_name", - "description": "family name is required" + "description": "family name must not exceed 200 characters", + "reason": "INVALID_LENGTH" } ] } @@ -246,23 +274,25 @@ gRPC / connectRPC example: ``` HTTP/2.0 200 OK Content-Type: application/grpc -Grpc-Message: missing required information for the creation of the user +Grpc-Message: invalid information provided for the creation of the user Grpc-Status: 3 { - "code": "user_missing_information", - "message": "missing required information for the creation of the user", + "code": "user_invalid_information", + "message": "invalid or missing information provided for the creation of the user", "details": [ { "@type": "type.googleapis.com/google.rpc.BadRequest", "fieldViolations": [ { "field": "given_name", - "description": "given name is required" + "description": "given name is required", + "reason": "MISSING_VALUE" }, { "field": "family_name", - "description": "family name is required" + "description": "family name must not exceed 200 characters", + "reason": "INVALID_LENGTH" } ] } @@ -292,7 +322,7 @@ Grpc-Status: 3 // - user.write // // Error Codes: -// - user_missing_information: The request is missing required information (either given_name, family_name and/or email) for the creation of the user. Check error details for the missing fields. +// - user_missing_information: The request is missing required information (either given_name, family_name and/or email) or contains invalid data for the creation of the user. Check error details for the missing or invalid fields. // - user_already_exists: The user already exists. The username must be unique. // - invalid_request: Your request does not have a valid format. Check error details for the reason. // - permission_denied: You do not have the required permissions to access the requested resource. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6fafd3dd6f..ce8b9aff89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -165,7 +165,7 @@ ZITADEL serves traffic as soon as you can see the following log line: ### Backend/login By executing the commands from this section, you run everything you need to develop the ZITADEL backend locally. -Using [Docker Compose](https://docs.docker.com/compose/), you run a [CockroachDB](https://www.cockroachlabs.com/docs/stable/start-a-local-cluster-in-docker-mac.html) on your local machine. +Using [Docker Compose](https://docs.docker.com/compose/), you run a [PostgreSQL](https://www.postgresql.org/download/) on your local machine. With [make](https://www.gnu.org/software/make/), you build a debuggable ZITADEL binary and run it using [delve](https://github.com/go-delve/delve). Then, you test your changes via the console your binary is serving at http://localhost:8080 and by verifying the database. Once you are happy with your changes, you run end-to-end tests and tear everything down. @@ -200,7 +200,7 @@ make compile You can now run and debug the binary in .artifacts/zitadel/zitadel using your favourite IDE, for example GoLand. You can test if ZITADEL does what you expect by using the UI at http://localhost:8080/ui/console. -Also, you can verify the data by running `cockroach sql --database zitadel --insecure` and running SQL queries. +Also, you can verify the data by running `psql "host=localhost dbname=zitadel sslmode=disable"` and running SQL queries. #### Run Local Unit Tests @@ -216,12 +216,6 @@ Integration tests are run as gRPC clients against a running ZITADEL server binar The server binary is typically [build with coverage enabled](https://go.dev/doc/build-cover). It is also possible to run a ZITADEL sever in a debugger and run the integrations tests like that. In order to run the server, a database is required. -The database flavor can **optionally** be set in the environment to `cockroach` or `postgres`. The default is `postgres`. - -```bash -export INTEGRATION_DB_FLAVOR="cockroach" -``` - In order to prepare the local system, the following will bring up the database, builds a coverage binary, initializes the database and starts the sever. ```bash @@ -306,7 +300,7 @@ docker compose --file ./e2e/config/host.docker.internal/docker-compose.yaml down ### Console By executing the commands from this section, you run everything you need to develop the console locally. -Using [Docker Compose](https://docs.docker.com/compose/), you run [CockroachDB](https://www.cockroachlabs.com/docs/stable/start-a-local-cluster-in-docker-mac.html) and the [latest release of ZITADEL](https://github.com/zitadel/zitadel/releases/latest) on your local machine. +Using [Docker Compose](https://docs.docker.com/compose/), you run [PostgreSQL](https://www.postgresql.org/download/) and the [latest release of ZITADEL](https://github.com/zitadel/zitadel/releases/latest) on your local machine. You use the ZITADEL container as backend for your console. The console is run in your [Node](https://nodejs.org/en/about/) environment using [a local development server for Angular](https://angular.io/cli/serve#ng-serve), so you have fast feedback about your changes. diff --git a/LICENSE b/LICENSE index a1ea99bb88..bae94e189e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,661 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. - 1. Definitions. + Preamble - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + The precise terms and conditions for copying, distribution and +modification follow. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + TERMS AND CONDITIONS - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + 0. Definitions. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "This License" refers to version 3 of the GNU Affero General Public License. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + A "covered work" means either the unmodified Program or a work based +on the Program. - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + 1. Source Code. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. - END OF TERMS AND CONDITIONS + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. - APPENDIX: How to apply the Apache License to your work. + The Corresponding Source for a work in source code form is that +same work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + 2. Basic Permissions. - Copyright 2020 CAOS AG + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. - http://www.apache.org/licenses/LICENSE-2.0 + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/LICENSING.md b/LICENSING.md new file mode 100644 index 0000000000..9cad2082f8 --- /dev/null +++ b/LICENSING.md @@ -0,0 +1,25 @@ +# Licensing Policy + +This repository is licensed under the [GNU Affero General Public License v3.0](LICENSE) (AGPL-3.0-only). We use the [SPDX License List](https://spdx.org/licenses/) for standard license naming. + +## AGPL-3.0-only Compliance + +ZITADEL is open-source software intended for community use. Determining your application's compliance with the AGPL-3.0-only license is your responsibility. + +**We strongly recommend consulting with legal counsel or licensing specialists to ensure your usage of ZITADEL, and any other integrated open-source projects, adheres to their respective licenses. AGPL-3.0-only compliance can be complex.** + +If your application triggers AGPL-3.0-only obligations and you wish to avoid them (e.g., you do not plan to open-source your modifications or application), please [contact us](https://zitadel.com/contact) to discuss commercial licensing options. Using ZITADEL without verifying your license compliance is at your own risk. + +## Exceptions to AGPL-3.0-only + +The following files and directories, including their subdirectories, are licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0): + +``` +proto/ +``` + +## Community Contributions + +To maintain a clear licensing structure and facilitate community contributions, all contributions must be licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) to be accepted. By submitting a contribution, you agree to this licensing. + +This approach avoids the need for a Contributor License Agreement (CLA) while ensuring clarity regarding license terms. We will only accept contributions licensed under Apache 2.0. diff --git a/Makefile b/Makefile index 27e76c0614..b5145cef3d 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,9 @@ COMMIT_SHA ?= $(shell git rev-parse HEAD) ZITADEL_IMAGE ?= zitadel:local GOCOVERDIR = tmp/coverage -INTEGRATION_DB_FLAVOR ?= postgres ZITADEL_MASTERKEY ?= MasterkeyNeedsToHave32Characters -export GOCOVERDIR INTEGRATION_DB_FLAVOR ZITADEL_MASTERKEY +export GOCOVERDIR ZITADEL_MASTERKEY .PHONY: compile compile: core_build console_build compile_pipeline @@ -113,7 +112,7 @@ core_unit_test: .PHONY: core_integration_db_up core_integration_db_up: - docker compose -f internal/integration/config/docker-compose.yaml up --pull always --wait $${INTEGRATION_DB_FLAVOR} cache + docker compose -f internal/integration/config/docker-compose.yaml up --pull always --wait cache .PHONY: core_integration_db_down core_integration_db_down: @@ -123,13 +122,13 @@ core_integration_db_down: core_integration_setup: go build -cover -race -tags integration -o zitadel.test main.go mkdir -p $${GOCOVERDIR} - GORACE="halt_on_error=1" ./zitadel.test init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml - GORACE="halt_on_error=1" ./zitadel.test setup --masterkeyFromEnv --init-projections --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml --steps internal/integration/config/steps.yaml + GORACE="halt_on_error=1" ./zitadel.test init --config internal/integration/config/zitadel.yaml --config internal/integration/config/postgres.yaml + GORACE="halt_on_error=1" ./zitadel.test setup --masterkeyFromEnv --init-projections --config internal/integration/config/zitadel.yaml --config internal/integration/config/postgres.yaml --steps internal/integration/config/steps.yaml .PHONY: core_integration_server_start core_integration_server_start: core_integration_setup GORACE="log_path=tmp/race.log" \ - ./zitadel.test start --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml \ + ./zitadel.test start --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/postgres.yaml \ > tmp/zitadel.log 2>&1 \ & printf $$! > tmp/zitadel.pid diff --git a/README.md b/README.md index 5d4aecf441..285e50964c 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Yet it offers everything you need for a customer identity ([CIAM](https://zitade - [Actions](https://zitadel.com/docs/apis/actions/introduction) to react on events with custom code and extended ZITADEL for you needs - [Branding](https://zitadel.com/docs/guides/manage/customize/branding) for a uniform user experience across multiple organizations - [Self-service](https://zitadel.com/docs/concepts/features/selfservice) for end-users, business customers, and administrators -- [CockroachDB](https://www.cockroachlabs.com/) or a [Postgres](https://www.postgresql.org/) database as reliable and widespread storage option +- [Postgres](https://www.postgresql.org/) database as reliable and widespread storage option ## Features @@ -151,7 +151,7 @@ Self-Service - [Administration UI (Console)](https://zitadel.com/docs/guides/manage/console/overview) Deployment -- [Postgres](https://zitadel.com/docs/self-hosting/manage/database#postgres) (version >= 14) or [CockroachDB](https://zitadel.com/docs/self-hosting/manage/database#cockroach) (version latest stable) +- [Postgres](https://zitadel.com/docs/self-hosting/manage/database#postgres) (version >= 14) - [Zero Downtime Updates](https://zitadel.com/docs/concepts/architecture/solution#zero-downtime-updates) - [High scalability](https://zitadel.com/docs/self-hosting/manage/production) @@ -168,7 +168,7 @@ Join our [Discord Chat](https://zitadel.com/chat) to get help. -Made with [contrib.rocks](https://contrib.rocks). +Made with [contrib.rocks](https://contrib.rocks/preview?repo=zitadel/zitadel). ## Showcase diff --git a/build/Dockerfile b/build/Dockerfile index 4e984fe8e6..769f04023e 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -27,6 +27,7 @@ COPY --from=artifact /etc/ssl/certs /etc/ssl/certs COPY --from=artifact /app/zitadel /app/zitadel HEALTHCHECK NONE +EXPOSE 8080 USER zitadel -ENTRYPOINT ["/app/zitadel"] \ No newline at end of file +ENTRYPOINT ["/app/zitadel"] diff --git a/build/workflow.Dockerfile b/build/workflow.Dockerfile index 47866f2691..2286531192 100644 --- a/build/workflow.Dockerfile +++ b/build/workflow.Dockerfile @@ -199,7 +199,6 @@ ENV PATH="/go/bin:/usr/local/go/bin:${PATH}" WORKDIR /go/src/github.com/zitadel/zitadel # default vars -ENV DB_FLAVOR=postgres ENV POSTGRES_USER=zitadel ENV POSTGRES_DB=zitadel ENV POSTGRES_PASSWORD=postgres @@ -231,12 +230,6 @@ COPY --from=test-core-unit /go/src/github.com/zitadel/zitadel/profile.cov /cover # integration test core # ####################################### FROM test-core-base AS test-core-integration -ENV DB_FLAVOR=cockroach - -# install cockroach -COPY --from=cockroachdb/cockroach:latest-v24.3 /cockroach/cockroach /usr/local/bin/ -ENV COCKROACH_BINARY=/cockroach/cockroach - ENV ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters COPY build/core-integration-test.sh /usr/local/bin/run-tests.sh diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 2eadfa77fd..123f468218 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -113,67 +113,36 @@ PublicHostHeaders: # ZITADEL_PUBLICHOSTHEADERS WebAuthNName: ZITADEL # ZITADEL_WEBAUTHNNAME Database: - # CockroachDB is the default database of ZITADEL - cockroach: - Host: localhost # ZITADEL_DATABASE_COCKROACH_HOST - Port: 26257 # ZITADEL_DATABASE_COCKROACH_PORT - Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE - MaxOpenConns: 5 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS - MaxIdleConns: 2 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS - MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME - MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME - Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS + # Postgres is the default database of ZITADEL + postgres: + Host: localhost # ZITADEL_DATABASE_POSTGRES_HOST + Port: 5432 # ZITADEL_DATABASE_POSTGRES_PORT + Database: zitadel # ZITADEL_DATABASE_POSTGRES_DATABASE + MaxOpenConns: 10 # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS + MaxIdleConns: 5 # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS + MaxConnLifetime: 30m # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME + MaxConnIdleTime: 5m # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME + Options: "" # ZITADEL_DATABASE_POSTGRES_OPTIONS User: - Username: zitadel # ZITADEL_DATABASE_COCKROACH_USER_USERNAME - Password: "" # ZITADEL_DATABASE_COCKROACH_USER_PASSWORD + Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME + Password: "" # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD SSL: - Mode: disable # ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE - RootCert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT - Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT - Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY + Mode: disable # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE + RootCert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT + Cert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT + Key: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY Admin: # By default, ExistingDatabase is not specified in the connection string # If the connection resolves to a database that is not existing in your system, configure an existing one here - # It is used in zitadel init to connect to cockroach and create a dedicated database for ZITADEL. - ExistingDatabase: # ZITADEL_DATABASE_COCKROACH_ADMIN_EXISTINGDATABASE - Username: root # ZITADEL_DATABASE_COCKROACH_ADMIN_USERNAME - Password: "" # ZITADEL_DATABASE_COCKROACH_ADMIN_PASSWORD - SSL: - Mode: disable # ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_MODE - RootCert: "" # ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_ROOTCERT - Cert: "" # ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_CERT - Key: "" # ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_KEY - # Postgres is used as soon as a value is set - # The values describe the possible fields to set values - postgres: - Host: # ZITADEL_DATABASE_POSTGRES_HOST - Port: # ZITADEL_DATABASE_POSTGRES_PORT - Database: # ZITADEL_DATABASE_POSTGRES_DATABASE - MaxOpenConns: # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS - MaxIdleConns: # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS - MaxConnLifetime: # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME - MaxConnIdleTime: # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME - Options: # ZITADEL_DATABASE_POSTGRES_OPTIONS - User: - Username: # ZITADEL_DATABASE_POSTGRES_USER_USERNAME - Password: # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD - SSL: - Mode: # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE - RootCert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT - Cert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT - Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY - Admin: - # The default ExistingDatabase is postgres - # If your db system doesn't have a database named postgres, configure an existing database here # It is used in zitadel init to connect to postgres and create a dedicated database for ZITADEL. ExistingDatabase: # ZITADEL_DATABASE_POSTGRES_ADMIN_EXISTINGDATABASE - Username: # ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME - Password: # ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD + Username: postgres # ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME + Password: postgres # ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD SSL: - Mode: # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE - RootCert: # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_ROOTCERT - Cert: # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_CERT - Key: # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_KEY + Mode: disable # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE + RootCert: "" # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_ROOTCERT + Cert: "" # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_CERT + Key: "" # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_KEY # Caches are EXPERIMENTAL. The following config may have breaking changes in the future. # If no config is provided, caching is disabled by default. @@ -447,19 +416,30 @@ Projections: Notifications: # Notifications can be processed by either a sequential mode (legacy) or a new parallel mode. # The parallel mode is currently only recommended for Postgres databases. - # For CockroachDB, the sequential mode is recommended, see: https://github.com/zitadel/zitadel/issues/9002 # If legacy mode is enabled, the worker config below is ignored. LegacyEnabled: true # ZITADEL_NOTIFICATIONS_LEGACYENABLED # The amount of workers processing the notification request events. # If set to 0, no notification request events will be handled. This can be useful when running in # multi binary / pod setup and allowing only certain executables to process the events. - Workers: 1 # ZITADEL_NOTIFIACATIONS_WORKERS + Workers: 1 # ZITADEL_NOTIFICATIONS_WORKERS # The maximum duration a job can do it's work before it is considered as failed. - TransactionDuration: 10s # ZITADEL_NOTIFIACATIONS_TRANSACTIONDURATION + TransactionDuration: 10s # ZITADEL_NOTIFICATIONS_TRANSACTIONDURATION # Automatically cancel the notification after the amount of failed attempts - MaxAttempts: 3 # ZITADEL_NOTIFIACATIONS_MAXATTEMPTS + MaxAttempts: 3 # ZITADEL_NOTIFICATIONS_MAXATTEMPTS # Automatically cancel the notification if it cannot be handled within a specific time - MaxTtl: 5m # ZITADEL_NOTIFIACATIONS_MAXTTL + MaxTtl: 5m # ZITADEL_NOTIFICATIONS_MAXTTL + +Executions: + # The amount of workers processing the execution request events. + # If set to 0, no execution request events will be handled. This can be useful when running in + # multi binary / pod setup and allowing only certain executables to process the events. + Workers: 1 # ZITADEL_EXECUTIONS_WORKERS + # The maximum duration a job can do it's work before it is considered as failed. + # This maximum duration is prioritized in case that the sum of the target's timeouts is higher, + # to limit the runtime of a singular execution. + TransactionDuration: 10s # ZITADEL_EXECUTIONS_TRANSACTIONDURATION + # Automatically cancel the notification if it cannot be handled within a specific time + MaxTtl: 5m # ZITADEL_EXECUTIONS_MAXTTL Auth: # See Projections.BulkLimit @@ -697,6 +677,7 @@ SystemDefaults: # - "bcrypt" # - "md5" # md5Crypt with salt and password shuffling. # - "md5plain" # md5 digest of a password without salt + # - "md5salted" # md5 digest of a salted password # - "scrypt" # - "pbkdf2" # verifier for all pbkdf2 hash modes. SecretHasher: @@ -968,16 +949,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 @@ -1110,7 +1090,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. @@ -1732,6 +1731,298 @@ InternalAuthZ: - "user.grant.read" - "user.membership.read" +SystemAuthZ: + RolePermissionMappings: + - Role: "SYSTEM_OWNER" + Permissions: + - "system.instance.read" + - "system.instance.write" + - "system.instance.delete" + - "system.domain.read" + - "system.domain.write" + - "system.domain.delete" + - "system.debug.read" + - "system.debug.write" + - "system.debug.delete" + - "system.feature.read" + - "system.feature.write" + - "system.feature.delete" + - "system.limits.write" + - "system.limits.delete" + - "system.quota.write" + - "system.quota.delete" + - "system.iam.member.read" + - Role: "SYSTEM_OWNER_VIEWER" + Permissions: + - "system.instance.read" + - "system.domain.read" + - "system.debug.read" + - "system.feature.read" + - "system.iam.member.read" + - Role: "IAM_OWNER" + Permissions: + - "iam.read" + - "iam.write" + - "iam.policy.read" + - "iam.policy.write" + - "iam.policy.delete" + - "iam.member.read" + - "iam.member.write" + - "iam.member.delete" + - "iam.idp.read" + - "iam.idp.write" + - "iam.idp.delete" + - "iam.action.read" + - "iam.action.write" + - "iam.action.delete" + - "iam.flow.read" + - "iam.flow.write" + - "iam.flow.delete" + - "iam.feature.read" + - "iam.feature.write" + - "iam.feature.delete" + - "iam.restrictions.read" + - "iam.restrictions.write" + - "iam.web_key.write" + - "iam.web_key.delete" + - "iam.web_key.read" + - "iam.debug.write" + - "iam.debug.read" + - "org.read" + - "org.global.read" + - "org.create" + - "org.write" + - "org.delete" + - "org.member.read" + - "org.member.write" + - "org.member.delete" + - "org.idp.read" + - "org.idp.write" + - "org.idp.delete" + - "org.action.read" + - "org.action.write" + - "org.action.delete" + - "org.flow.read" + - "org.flow.write" + - "org.flow.delete" + - "org.feature.read" + - "org.feature.write" + - "org.feature.delete" + - "user.read" + - "user.global.read" + - "user.write" + - "user.delete" + - "user.grant.read" + - "user.grant.write" + - "user.grant.delete" + - "user.membership.read" + - "user.credential.write" + - "user.passkey.write" + - "user.feature.read" + - "user.feature.write" + - "user.feature.delete" + - "policy.read" + - "policy.write" + - "policy.delete" + - "project.read" + - "project.create" + - "project.write" + - "project.delete" + - "project.member.read" + - "project.member.write" + - "project.member.delete" + - "project.role.read" + - "project.role.write" + - "project.role.delete" + - "project.app.read" + - "project.app.write" + - "project.app.delete" + - "project.grant.read" + - "project.grant.write" + - "project.grant.delete" + - "project.grant.member.read" + - "project.grant.member.write" + - "project.grant.member.delete" + - "events.read" + - "milestones.read" + - "session.read" + - "session.delete" + - "action.target.read" + - "action.target.write" + - "action.target.delete" + - "action.execution.read" + - "action.execution.write" + - "userschema.read" + - "userschema.write" + - "userschema.delete" + - "session.read" + - "session.delete" + - Role: "IAM_OWNER_VIEWER" + Permissions: + - "iam.read" + - "iam.policy.read" + - "iam.member.read" + - "iam.idp.read" + - "iam.action.read" + - "iam.flow.read" + - "iam.restrictions.read" + - "iam.feature.read" + - "iam.web_key.read" + - "iam.debug.read" + - "org.read" + - "org.member.read" + - "org.idp.read" + - "org.action.read" + - "org.flow.read" + - "org.feature.read" + - "user.read" + - "user.global.read" + - "user.grant.read" + - "user.membership.read" + - "user.feature.read" + - "policy.read" + - "project.read" + - "project.member.read" + - "project.role.read" + - "project.app.read" + - "project.grant.read" + - "project.grant.member.read" + - "events.read" + - "milestones.read" + - "action.target.read" + - "action.execution.read" + - "userschema.read" + - "session.read" + - Role: "IAM_ORG_MANAGER" + Permissions: + - "org.read" + - "org.global.read" + - "org.create" + - "org.write" + - "org.delete" + - "org.member.read" + - "org.member.write" + - "org.member.delete" + - "org.idp.read" + - "org.idp.write" + - "org.idp.delete" + - "org.action.read" + - "org.action.write" + - "org.action.delete" + - "org.flow.read" + - "org.flow.write" + - "org.flow.delete" + - "org.feature.read" + - "org.feature.write" + - "org.feature.delete" + - "user.read" + - "user.global.read" + - "user.write" + - "user.delete" + - "user.grant.read" + - "user.grant.write" + - "user.grant.delete" + - "user.membership.read" + - "user.credential.write" + - "user.passkey.write" + - "user.feature.read" + - "user.feature.write" + - "user.feature.delete" + - "policy.read" + - "policy.write" + - "policy.delete" + - "project.read" + - "project.create" + - "project.write" + - "project.delete" + - "project.member.read" + - "project.member.write" + - "project.member.delete" + - "project.role.read" + - "project.role.write" + - "project.role.delete" + - "project.app.read" + - "project.app.write" + - "project.app.delete" + - "project.grant.read" + - "project.grant.write" + - "project.grant.delete" + - "project.grant.member.read" + - "project.grant.member.write" + - "project.grant.member.delete" + - "session.delete" + - Role: "IAM_USER_MANAGER" + Permissions: + - "org.read" + - "org.global.read" + - "org.member.read" + - "org.member.delete" + - "user.read" + - "user.global.read" + - "user.write" + - "user.delete" + - "user.grant.read" + - "user.grant.write" + - "user.grant.delete" + - "user.membership.read" + - "user.passkey.write" + - "user.feature.read" + - "user.feature.write" + - "user.feature.delete" + - "project.read" + - "project.member.read" + - "project.role.read" + - "project.app.read" + - "project.grant.read" + - "project.grant.write" + - "project.grant.delete" + - "project.grant.member.read" + - "session.delete" + - Role: "IAM_ADMIN_IMPERSONATOR" + Permissions: + - "admin.impersonation" + - "impersonation" + - Role: "IAM_END_USER_IMPERSONATOR" + Permissions: + - "impersonation" + - Role: "IAM_LOGIN_CLIENT" + Permissions: + - "iam.read" + - "iam.policy.read" + - "iam.member.read" + - "iam.member.write" + - "iam.idp.read" + - "iam.feature.read" + - "iam.restrictions.read" + - "org.read" + - "org.member.read" + - "org.member.write" + - "org.idp.read" + - "org.feature.read" + - "user.read" + - "user.write" + - "user.grant.read" + - "user.grant.write" + - "user.membership.read" + - "user.credential.write" + - "user.passkey.write" + - "user.feature.read" + - "policy.read" + - "project.read" + - "project.member.read" + - "project.member.write" + - "project.role.read" + - "project.app.read" + - "project.member.read" + - "project.member.write" + - "project.grant.read" + - "project.grant.member.read" + - "project.grant.member.write" + - "session.read" + - "session.link" + - "session.delete" + - "userschema.read" + # If a new projection is introduced it will be prefilled during the setup process (if enabled) # This can prevent serving outdated data after a version upgrade, but might require a longer setup / upgrade process: # https://zitadel.com/docs/self-hosting/manage/updating_scaling diff --git a/cmd/initialise/config.go b/cmd/initialise/config.go index 3fe7173860..899018ddcb 100644 --- a/cmd/initialise/config.go +++ b/cmd/initialise/config.go @@ -19,7 +19,7 @@ func MustNewConfig(v *viper.Viper) *Config { config := new(Config) err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( - database.DecodeHook, + database.DecodeHook(false), mapstructure.TextUnmarshallerHookFunc(), )), ) diff --git a/cmd/initialise/init.go b/cmd/initialise/init.go index 02fd481eab..cc505325a9 100644 --- a/cmd/initialise/init.go +++ b/cmd/initialise/init.go @@ -12,20 +12,17 @@ import ( ) var ( - //go:embed sql/cockroach/* - //go:embed sql/postgres/* + //go:embed sql/*.sql stmts embed.FS createUserStmt string grantStmt string - settingsStmt string databaseStmt string createEventstoreStmt string createProjectionsStmt string createSystemStmt string createEncryptionKeysStmt string createEventsStmt string - createSystemSequenceStmt string createUniqueConstraints string roleAlreadyExistsCode = "42710" @@ -39,7 +36,7 @@ func New() *cobra.Command { Long: `Sets up the minimum requirements to start ZITADEL. Prerequisites: -- database (PostgreSql or cockroachdb) +- PostgreSql database The user provided by flags needs privileges to - create the database if it does not exist @@ -53,7 +50,7 @@ The user provided by flags needs privileges to }, } - cmd.AddCommand(newZitadel(), newDatabase(), newUser(), newGrant(), newSettings()) + cmd.AddCommand(newZitadel(), newDatabase(), newUser(), newGrant()) return cmd } @@ -62,7 +59,6 @@ func InitAll(ctx context.Context, config *Config) { VerifyUser(config.Database.Username(), config.Database.Password()), VerifyDatabase(config.Database.DatabaseName()), VerifyGrant(config.Database.DatabaseName(), config.Database.Username()), - VerifySettings(config.Database.DatabaseName(), config.Database.Username()), ) logging.OnError(err).Fatal("unable to initialize the database") @@ -73,7 +69,7 @@ func InitAll(ctx context.Context, config *Config) { func initialise(ctx context.Context, config database.Config, steps ...func(context.Context, *database.DB) error) error { logging.Info("initialization started") - err := ReadStmts(config.Type()) + err := ReadStmts() if err != nil { return err } @@ -97,58 +93,48 @@ func Init(ctx context.Context, db *database.DB, steps ...func(context.Context, * return nil } -func ReadStmts(typ string) (err error) { - createUserStmt, err = readStmt(typ, "01_user") +func ReadStmts() (err error) { + createUserStmt, err = readStmt("01_user") if err != nil { return err } - databaseStmt, err = readStmt(typ, "02_database") + databaseStmt, err = readStmt("02_database") if err != nil { return err } - grantStmt, err = readStmt(typ, "03_grant_user") + grantStmt, err = readStmt("03_grant_user") if err != nil { return err } - createEventstoreStmt, err = readStmt(typ, "04_eventstore") + createEventstoreStmt, err = readStmt("04_eventstore") if err != nil { return err } - createProjectionsStmt, err = readStmt(typ, "05_projections") + createProjectionsStmt, err = readStmt("05_projections") if err != nil { return err } - createSystemStmt, err = readStmt(typ, "06_system") + createSystemStmt, err = readStmt("06_system") if err != nil { return err } - createEncryptionKeysStmt, err = readStmt(typ, "07_encryption_keys_table") + createEncryptionKeysStmt, err = readStmt("07_encryption_keys_table") if err != nil { return err } - createEventsStmt, err = readStmt(typ, "08_events_table") + createEventsStmt, err = readStmt("08_events_table") if err != nil { return err } - createSystemSequenceStmt, err = readStmt(typ, "09_system_sequence") - if err != nil { - return err - } - - createUniqueConstraints, err = readStmt(typ, "10_unique_constraints_table") - if err != nil { - return err - } - - settingsStmt, err = readStmt(typ, "11_settings") + createUniqueConstraints, err = readStmt("10_unique_constraints_table") if err != nil { return err } @@ -156,7 +142,7 @@ func ReadStmts(typ string) (err error) { return nil } -func readStmt(typ, step string) (string, error) { - stmt, err := stmts.ReadFile("sql/" + typ + "/" + step + ".sql") +func readStmt(step string) (string, error) { + stmt, err := stmts.ReadFile("sql/" + step + ".sql") return string(stmt), err } diff --git a/cmd/initialise/sql/cockroach/01_user.sql b/cmd/initialise/sql/01_user.sql similarity index 56% rename from cmd/initialise/sql/cockroach/01_user.sql rename to cmd/initialise/sql/01_user.sql index 4e621216ce..7be2d4ae4d 100644 --- a/cmd/initialise/sql/cockroach/01_user.sql +++ b/cmd/initialise/sql/01_user.sql @@ -1,2 +1,2 @@ -- replace %[1]s with the name of the user -CREATE USER IF NOT EXISTS "%[1]s" \ No newline at end of file +CREATE USER "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/02_database.sql b/cmd/initialise/sql/02_database.sql similarity index 54% rename from cmd/initialise/sql/cockroach/02_database.sql rename to cmd/initialise/sql/02_database.sql index 6103b95b31..172913661b 100644 --- a/cmd/initialise/sql/cockroach/02_database.sql +++ b/cmd/initialise/sql/02_database.sql @@ -1,2 +1,2 @@ -- replace %[1]s with the name of the database -CREATE DATABASE IF NOT EXISTS "%[1]s"; +CREATE DATABASE "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/03_grant_user.sql b/cmd/initialise/sql/03_grant_user.sql similarity index 100% rename from cmd/initialise/sql/postgres/03_grant_user.sql rename to cmd/initialise/sql/03_grant_user.sql diff --git a/cmd/initialise/sql/cockroach/04_eventstore.sql b/cmd/initialise/sql/04_eventstore.sql similarity index 100% rename from cmd/initialise/sql/cockroach/04_eventstore.sql rename to cmd/initialise/sql/04_eventstore.sql diff --git a/cmd/initialise/sql/cockroach/05_projections.sql b/cmd/initialise/sql/05_projections.sql similarity index 100% rename from cmd/initialise/sql/cockroach/05_projections.sql rename to cmd/initialise/sql/05_projections.sql diff --git a/cmd/initialise/sql/cockroach/06_system.sql b/cmd/initialise/sql/06_system.sql similarity index 100% rename from cmd/initialise/sql/cockroach/06_system.sql rename to cmd/initialise/sql/06_system.sql diff --git a/cmd/initialise/sql/cockroach/07_encryption_keys_table.sql b/cmd/initialise/sql/07_encryption_keys_table.sql similarity index 100% rename from cmd/initialise/sql/cockroach/07_encryption_keys_table.sql rename to cmd/initialise/sql/07_encryption_keys_table.sql diff --git a/cmd/initialise/sql/postgres/08_events_table.sql b/cmd/initialise/sql/08_events_table.sql similarity index 100% rename from cmd/initialise/sql/postgres/08_events_table.sql rename to cmd/initialise/sql/08_events_table.sql diff --git a/cmd/initialise/sql/postgres/10_unique_constraints_table.sql b/cmd/initialise/sql/10_unique_constraints_table.sql similarity index 100% rename from cmd/initialise/sql/postgres/10_unique_constraints_table.sql rename to cmd/initialise/sql/10_unique_constraints_table.sql diff --git a/cmd/initialise/sql/README.md b/cmd/initialise/sql/README.md index b477c0fb73..b7a18f0f98 100644 --- a/cmd/initialise/sql/README.md +++ b/cmd/initialise/sql/README.md @@ -11,6 +11,5 @@ The sql-files in this folder initialize the ZITADEL database and user. These obj - 05_projections.sql: creates the schema needed to read the data - 06_system.sql: creates the schema needed for ZITADEL itself - 07_encryption_keys_table.sql: creates the table for encryption keys (for event data) -- files 08_enable_hash_sharded_indexes.sql and 09_events_table.sql must run in the same session - - 08_enable_hash_sharded_indexes.sql enables the [hash sharded index](https://www.cockroachlabs.com/docs/stable/hash-sharded-indexes.html) feature for this session - - 09_events_table.sql creates the table for eventsourcing +- 08_events_table.sql creates the table for eventsourcing +- 10_unique_constraints_table.sql creates the table to check unique constraints for events diff --git a/cmd/initialise/sql/cockroach/03_grant_user.sql b/cmd/initialise/sql/cockroach/03_grant_user.sql deleted file mode 100644 index de0d2743eb..0000000000 --- a/cmd/initialise/sql/cockroach/03_grant_user.sql +++ /dev/null @@ -1,4 +0,0 @@ --- replace the first %[1]s with the database --- replace the second \%[2]s with the user -GRANT ALL ON DATABASE "%[1]s" TO "%[2]s"; -GRANT SYSTEM VIEWACTIVITY TO "%[2]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/08_events_table.sql b/cmd/initialise/sql/cockroach/08_events_table.sql deleted file mode 100644 index ebaf18ce2a..0000000000 --- a/cmd/initialise/sql/cockroach/08_events_table.sql +++ /dev/null @@ -1,116 +0,0 @@ -CREATE TABLE IF NOT EXISTS eventstore.events2 ( - instance_id TEXT NOT NULL - , aggregate_type TEXT NOT NULL - , aggregate_id TEXT NOT NULL - - , event_type TEXT NOT NULL - , "sequence" BIGINT NOT NULL - , revision SMALLINT NOT NULL - , created_at TIMESTAMPTZ NOT NULL - , payload JSONB - , creator TEXT NOT NULL - , "owner" TEXT NOT NULL - - , "position" DECIMAL NOT NULL - , in_tx_order INTEGER NOT NULL - - , PRIMARY KEY (instance_id, aggregate_type, aggregate_id, "sequence") - , INDEX es_active_instances (created_at DESC) STORING ("position") - , INDEX es_wm (aggregate_id, instance_id, aggregate_type, event_type) - , INDEX es_projection (instance_id, aggregate_type, event_type, "position" DESC) -); - --- represents an event to be created. -CREATE TYPE IF NOT EXISTS eventstore.command AS ( - instance_id TEXT - , aggregate_type TEXT - , aggregate_id TEXT - , command_type TEXT - , revision INT2 - , payload JSONB - , creator TEXT - , owner TEXT -); - -CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$ -SELECT - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - , ("c").command_type AS event_type - , cs.sequence + ROW_NUMBER() OVER (PARTITION BY ("c").instance_id, ("c").aggregate_type, ("c").aggregate_id ORDER BY ("c").in_tx_order) AS sequence - , ("c").revision - , hlc_to_timestamp(cluster_logical_timestamp()) AS created_at - , ("c").payload - , ("c").creator - , cs.owner - , cluster_logical_timestamp() AS position - , ("c").in_tx_order -FROM ( - SELECT - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - , ("c").command_type - , ("c").revision - , ("c").payload - , ("c").creator - , ("c").owner - , ROW_NUMBER() OVER () AS in_tx_order - FROM - UNNEST(commands) AS "c" -) AS "c" -JOIN ( - SELECT - cmds.instance_id - , cmds.aggregate_type - , cmds.aggregate_id - , CASE WHEN (e.owner IS NOT NULL OR e.owner <> '') THEN e.owner ELSE command_owners.owner END AS owner - , COALESCE(MAX(e.sequence), 0) AS sequence - FROM ( - SELECT DISTINCT - ("cmds").instance_id - , ("cmds").aggregate_type - , ("cmds").aggregate_id - , ("cmds").owner - FROM UNNEST(commands) AS "cmds" - ) AS cmds - LEFT JOIN eventstore.events2 AS e - ON cmds.instance_id = e.instance_id - AND cmds.aggregate_type = e.aggregate_type - AND cmds.aggregate_id = e.aggregate_id - JOIN ( - SELECT - DISTINCT ON ( - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - ) - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - , ("c").owner - FROM - UNNEST(commands) AS "c" - ) AS command_owners ON - cmds.instance_id = command_owners.instance_id - AND cmds.aggregate_type = command_owners.aggregate_type - AND cmds.aggregate_id = command_owners.aggregate_id - GROUP BY - cmds.instance_id - , cmds.aggregate_type - , cmds.aggregate_id - , 4 -- owner -) AS cs - ON ("c").instance_id = cs.instance_id - AND ("c").aggregate_type = cs.aggregate_type - AND ("c").aggregate_id = cs.aggregate_id -ORDER BY - in_tx_order -$$ LANGUAGE SQL; - -CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command[]) RETURNS SETOF eventstore.events2 AS $$ - INSERT INTO eventstore.events2 - SELECT * FROM eventstore.commands_to_events(commands) - RETURNING * -$$ LANGUAGE SQL; \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/09_system_sequence.sql b/cmd/initialise/sql/cockroach/09_system_sequence.sql deleted file mode 100644 index 596e887664..0000000000 --- a/cmd/initialise/sql/cockroach/09_system_sequence.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE SEQUENCE IF NOT EXISTS eventstore.system_seq diff --git a/cmd/initialise/sql/cockroach/10_unique_constraints_table.sql b/cmd/initialise/sql/cockroach/10_unique_constraints_table.sql deleted file mode 100644 index 2594a248b7..0000000000 --- a/cmd/initialise/sql/cockroach/10_unique_constraints_table.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS eventstore.unique_constraints ( - instance_id TEXT, - unique_type TEXT, - unique_field TEXT, - PRIMARY KEY (instance_id, unique_type, unique_field) -) diff --git a/cmd/initialise/sql/cockroach/11_settings.sql b/cmd/initialise/sql/cockroach/11_settings.sql deleted file mode 100644 index 5fa9dd72f6..0000000000 --- a/cmd/initialise/sql/cockroach/11_settings.sql +++ /dev/null @@ -1,4 +0,0 @@ --- replace the first %[1]q with the database in double quotes --- replace the second \%[2]q with the user in double quotes$ --- For more information see technical advisory 10009 (https://zitadel.com/docs/support/advisory/a10009) -ALTER ROLE %[2]q IN DATABASE %[1]q SET enable_durable_locking_for_serializable = on; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/01_user.sql b/cmd/initialise/sql/postgres/01_user.sql deleted file mode 100644 index cd60b9a2cf..0000000000 --- a/cmd/initialise/sql/postgres/01_user.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE USER "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/02_database.sql b/cmd/initialise/sql/postgres/02_database.sql deleted file mode 100644 index 895a1f29d5..0000000000 --- a/cmd/initialise/sql/postgres/02_database.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE DATABASE "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/04_eventstore.sql b/cmd/initialise/sql/postgres/04_eventstore.sql deleted file mode 100644 index 3cb4fc0d3e..0000000000 --- a/cmd/initialise/sql/postgres/04_eventstore.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS eventstore; - -GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/05_projections.sql b/cmd/initialise/sql/postgres/05_projections.sql deleted file mode 100644 index 91ca6662ee..0000000000 --- a/cmd/initialise/sql/postgres/05_projections.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS projections; - -GRANT ALL ON ALL TABLES IN SCHEMA projections TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/06_system.sql b/cmd/initialise/sql/postgres/06_system.sql deleted file mode 100644 index 6c9138918b..0000000000 --- a/cmd/initialise/sql/postgres/06_system.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS system; - -GRANT ALL ON ALL TABLES IN SCHEMA system TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/07_encryption_keys_table.sql b/cmd/initialise/sql/postgres/07_encryption_keys_table.sql deleted file mode 100644 index 61cb617fdf..0000000000 --- a/cmd/initialise/sql/postgres/07_encryption_keys_table.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS system.encryption_keys ( - id TEXT NOT NULL - , key TEXT NOT NULL - - , PRIMARY KEY (id) -); diff --git a/cmd/initialise/sql/postgres/09_system_sequence.sql b/cmd/initialise/sql/postgres/09_system_sequence.sql deleted file mode 100644 index 15383b3878..0000000000 --- a/cmd/initialise/sql/postgres/09_system_sequence.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE SEQUENCE IF NOT EXISTS eventstore.system_seq; diff --git a/cmd/initialise/verify_database.go b/cmd/initialise/verify_database.go index 6e04e489f5..3e3bea9efa 100644 --- a/cmd/initialise/verify_database.go +++ b/cmd/initialise/verify_database.go @@ -19,7 +19,7 @@ func newDatabase() *cobra.Command { Long: `Sets up the ZITADEL database. Prerequisites: -- cockroachDB or postgreSQL +- postgreSQL The user provided by flags needs privileges to - create the database if it does not exist diff --git a/cmd/initialise/verify_database_test.go b/cmd/initialise/verify_database_test.go index d7da97847f..1899605e4f 100644 --- a/cmd/initialise/verify_database_test.go +++ b/cmd/initialise/verify_database_test.go @@ -8,7 +8,7 @@ import ( ) func Test_verifyDB(t *testing.T) { - err := ReadStmts("cockroach") //TODO: check all dialects + err := ReadStmts() if err != nil { t.Errorf("unable to read stmts: %v", err) t.FailNow() @@ -27,7 +27,7 @@ func Test_verifyDB(t *testing.T) { name: "doesn't exists, create fails", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", sql.ErrTxDone), + expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE \"zitadel\"", sql.ErrTxDone), ), database: "zitadel", }, @@ -37,7 +37,7 @@ func Test_verifyDB(t *testing.T) { name: "doesn't exists, create successful", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", nil), + expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE \"zitadel\"", nil), ), database: "zitadel", }, @@ -47,7 +47,7 @@ func Test_verifyDB(t *testing.T) { name: "already exists", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", nil), + expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE \"zitadel\"", nil), ), database: "zitadel", }, diff --git a/cmd/initialise/verify_grant.go b/cmd/initialise/verify_grant.go index a14a495bff..27f0bd4d08 100644 --- a/cmd/initialise/verify_grant.go +++ b/cmd/initialise/verify_grant.go @@ -19,7 +19,7 @@ func newGrant() *cobra.Command { Long: `Sets ALL grant to the database user. Prerequisites: -- cockroachDB or postgreSQL +- postgreSQL `, Run: func(cmd *cobra.Command, args []string) { config := MustNewConfig(viper.GetViper()) diff --git a/cmd/initialise/verify_settings.go b/cmd/initialise/verify_settings.go deleted file mode 100644 index 6f4ba7c074..0000000000 --- a/cmd/initialise/verify_settings.go +++ /dev/null @@ -1,45 +0,0 @@ -package initialise - -import ( - "context" - _ "embed" - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/internal/database" -) - -func newSettings() *cobra.Command { - return &cobra.Command{ - Use: "settings", - Short: "Ensures proper settings on the database", - Long: `Ensures proper settings on the database. - -Prerequisites: -- cockroachDB or postgreSQL - -Cockroach -- Sets enable_durable_locking_for_serializable to on for the zitadel user and database -`, - Run: func(cmd *cobra.Command, args []string) { - config := MustNewConfig(viper.GetViper()) - - err := initialise(cmd.Context(), config.Database, VerifySettings(config.Database.DatabaseName(), config.Database.Username())) - logging.OnError(err).Fatal("unable to set settings") - }, - } -} - -func VerifySettings(databaseName, username string) func(context.Context, *database.DB) error { - return func(ctx context.Context, db *database.DB) error { - if db.Type() == "postgres" { - return nil - } - logging.WithFields("user", username, "database", databaseName).Info("verify settings") - - return exec(ctx, db, fmt.Sprintf(settingsStmt, databaseName, username), nil) - } -} diff --git a/cmd/initialise/verify_user.go b/cmd/initialise/verify_user.go index 43bdb91420..3adca93e53 100644 --- a/cmd/initialise/verify_user.go +++ b/cmd/initialise/verify_user.go @@ -19,7 +19,7 @@ func newUser() *cobra.Command { Long: `Sets up the ZITADEL database user. Prerequisites: -- cockroachDB or postgreSQL +- postgreSQL The user provided by flags needs privileges to - create the database if it does not exist diff --git a/cmd/initialise/verify_user_test.go b/cmd/initialise/verify_user_test.go index 53b35e67db..40cde5baa2 100644 --- a/cmd/initialise/verify_user_test.go +++ b/cmd/initialise/verify_user_test.go @@ -8,7 +8,7 @@ import ( ) func Test_verifyUser(t *testing.T) { - err := ReadStmts("cockroach") //TODO: check all dialects + err := ReadStmts() if err != nil { t.Errorf("unable to read stmts: %v", err) t.FailNow() @@ -28,7 +28,7 @@ func Test_verifyUser(t *testing.T) { name: "doesn't exists, create fails", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\"", sql.ErrTxDone), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\"", sql.ErrTxDone), ), username: "zitadel-user", password: "", @@ -39,7 +39,7 @@ func Test_verifyUser(t *testing.T) { name: "correct without password", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\"", nil), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\"", nil), ), username: "zitadel-user", password: "", @@ -50,7 +50,7 @@ func Test_verifyUser(t *testing.T) { name: "correct with password", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\" WITH PASSWORD 'password'", nil), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\" WITH PASSWORD 'password'", nil), ), username: "zitadel-user", password: "password", @@ -61,7 +61,7 @@ func Test_verifyUser(t *testing.T) { name: "already exists", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\" WITH PASSWORD 'password'", nil), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\" WITH PASSWORD 'password'", nil), ), username: "zitadel-user", password: "", diff --git a/cmd/initialise/verify_zitadel.go b/cmd/initialise/verify_zitadel.go index 1ae85a21fa..78f28809c2 100644 --- a/cmd/initialise/verify_zitadel.go +++ b/cmd/initialise/verify_zitadel.go @@ -21,7 +21,7 @@ func newZitadel() *cobra.Command { Long: `initialize ZITADEL internals. Prerequisites: -- cockroachDB or postgreSQL with user and database +- postgreSQL with user and database `, Run: func(cmd *cobra.Command, args []string) { config := MustNewConfig(viper.GetViper()) @@ -32,7 +32,7 @@ Prerequisites: } func VerifyZitadel(ctx context.Context, db *database.DB, config database.Config) error { - err := ReadStmts(config.Type()) + err := ReadStmts() if err != nil { return err } @@ -68,11 +68,6 @@ func VerifyZitadel(ctx context.Context, db *database.DB, config database.Config) return err } - logging.WithFields().Info("verify system sequence") - if err := exec(ctx, conn, createSystemSequenceStmt, nil); err != nil { - return err - } - logging.WithFields().Info("verify unique constraints") if err := exec(ctx, conn, createUniqueConstraints, nil); err != nil { return err diff --git a/cmd/initialise/verify_zitadel_test.go b/cmd/initialise/verify_zitadel_test.go index 194911a179..7fccd4c0a2 100644 --- a/cmd/initialise/verify_zitadel_test.go +++ b/cmd/initialise/verify_zitadel_test.go @@ -9,7 +9,7 @@ import ( ) func Test_verifyEvents(t *testing.T) { - err := ReadStmts("cockroach") //TODO: check all dialects + err := ReadStmts() if err != nil { t.Errorf("unable to read stmts: %v", err) t.FailNow() diff --git a/cmd/key/key.go b/cmd/key/key.go index 1dba8fd969..a1cf15b34e 100644 --- a/cmd/key/key.go +++ b/cmd/key/key.go @@ -40,7 +40,7 @@ func newKey() *cobra.Command { Long: `create new encryption key(s) (encrypted by the provided master key) provide key(s) by YAML file and/or by argument Requirements: -- cockroachdb`, +- postgreSQL`, Example: `new -f keys.yaml new key1=somekey key2=anotherkey new -f keys.yaml key2=anotherkey`, diff --git a/cmd/mirror/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 4bc994605a..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 @@ -76,7 +77,7 @@ func mustNewConfig(v *viper.Viper, config any) { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), - database.DecodeHook, + database.DecodeHook(true), actions.HTTPConfigDecodeHook, hook.EnumHookFunc(internal_authz.MemberTypeString), mapstructure.TextUnmarshallerHookFunc(), diff --git a/cmd/mirror/defaults.yaml b/cmd/mirror/defaults.yaml index 7db91ecc0b..4d8a0a4eae 100644 --- a/cmd/mirror/defaults.yaml +++ b/cmd/mirror/defaults.yaml @@ -1,84 +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 - EventPushConnRatio: 0.33 # ZITADEL_DATABASE_COCKROACH_EVENTPUSHCONNRATIO - ProjectionSpoolerConnRatio: 0.33 # ZITADEL_DATABASE_COCKROACH_PROJECTIONSPOOLERCONNRATIO - MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME - MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME - Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS + 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: - cockroach: - Host: localhost # ZITADEL_DATABASE_COCKROACH_HOST - Port: 26257 # ZITADEL_DATABASE_COCKROACH_PORT - Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE - MaxOpenConns: 0 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS - MaxIdleConns: 0 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS - MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME - MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME - EventPushConnRatio: 0.01 # ZITADEL_DATABASE_COCKROACH_EVENTPUSHCONNRATIO - ProjectionSpoolerConnRatio: 0.5 # ZITADEL_DATABASE_COCKROACH_PROJECTIONSPOOLERCONNRATIO - Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS - User: - Username: zitadel # ZITADEL_DATABASE_COCKROACH_USER_USERNAME - Password: "" # ZITADEL_DATABASE_COCKROACH_USER_PASSWORD - SSL: - Mode: disable # ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE - RootCert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT - Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT - Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY - # Postgres is used as soon as a value is set - # The values describe the possible fields to set values postgres: - Host: # ZITADEL_DATABASE_POSTGRES_HOST - Port: # ZITADEL_DATABASE_POSTGRES_PORT - Database: # ZITADEL_DATABASE_POSTGRES_DATABASE - MaxOpenConns: # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS - MaxIdleConns: # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS - MaxConnLifetime: # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME - MaxConnIdleTime: # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME - Options: # ZITADEL_DATABASE_POSTGRES_OPTIONS + Host: localhost # ZITADEL_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_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: # 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 @@ -87,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 2bb0d52f45..d513990e10 100644 --- a/cmd/mirror/event.go +++ b/cmd/mirror/event.go @@ -4,7 +4,6 @@ import ( "context" "github.com/zitadel/zitadel/internal/v2/eventstore" - "github.com/zitadel/zitadel/internal/v2/projection" "github.com/zitadel/zitadel/internal/v2/readmodel" "github.com/zitadel/zitadel/internal/v2/system" mirror_event "github.com/zitadel/zitadel/internal/v2/system/mirror" @@ -30,39 +29,6 @@ func queryLastSuccessfulMigration(ctx context.Context, destinationES *eventstore return lastSuccess, nil } -func writeMigrationStart(ctx context.Context, sourceES *eventstore.EventStore, id string, destination string) (_ float64, err error) { - var cmd *eventstore.Command - if len(instanceIDs) > 0 { - cmd, err = mirror_event.NewStartedInstancesCommand(destination, instanceIDs) - if err != nil { - return 0, err - } - } else { - cmd = mirror_event.NewStartedSystemCommand(destination) - } - - var position projection.HighestPosition - - err = sourceES.Push( - ctx, - eventstore.NewPushIntent( - system.AggregateInstance, - eventstore.AppendAggregate( - system.AggregateOwner, - system.AggregateType, - id, - eventstore.CurrentSequenceMatches(0), - eventstore.AppendCommands(cmd), - ), - eventstore.PushReducer(&position), - ), - ) - if err != nil { - return 0, err - } - return position.Position, nil -} - func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position float64) error { return destinationES.Push( ctx, diff --git a/cmd/mirror/event_store.go b/cmd/mirror/event_store.go index 3825462126..41c529c025 100644 --- a/cmd/mirror/event_store.go +++ b/cmd/mirror/event_store.go @@ -14,6 +14,7 @@ import ( "github.com/zitadel/logging" db "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/database/dialect" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/v2/database" "github.com/zitadel/zitadel/internal/v2/eventstore" @@ -57,9 +58,9 @@ func copyEventstore(ctx context.Context, config *Migration) { func positionQuery(db *db.DB) string { switch db.Type() { - case "postgres": + case dialect.DatabaseTypePostgres: return "SELECT EXTRACT(EPOCH FROM clock_timestamp())" - case "cockroach": + case dialect.DatabaseTypeCockroach: return "SELECT cluster_logical_timestamp()" default: logging.WithFields("db_type", db.Type()).Fatal("database type not recognized") @@ -68,6 +69,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() @@ -80,9 +82,6 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { destConn, err := dest.Conn(ctx) logging.OnError(err).Fatal("unable to acquire dest connection") - sourceES := eventstore.NewEventstoreFromOne(postgres.New(source, &postgres.Config{ - MaxRetries: 3, - })) destinationES := eventstore.NewEventstoreFromOne(postgres.New(dest, &postgres.Config{ MaxRetries: 3, })) @@ -90,8 +89,14 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { previousMigration, err := queryLastSuccessfulMigration(ctx, destinationES, source.DatabaseName()) logging.OnError(err).Fatal("unable to query latest successful migration") - maxPosition, err := writeMigrationStart(ctx, sourceES, migrationID, dest.DatabaseName()) - logging.OnError(err).Fatal("unable to write migration started event") + var maxPosition float64 + err = source.QueryRowContext(ctx, + func(row *sql.Row) error { + return row.Scan(&maxPosition) + }, + "SELECT MAX(position) FROM eventstore.events2 "+instanceClause(), + ) + logging.OnError(err).Fatal("unable to query max position from source") logging.WithFields("from", previousMigration.Position, "to", maxPosition).Info("start event migration") @@ -126,7 +131,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 } @@ -198,6 +206,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/mirror.go b/cmd/mirror/mirror.go index 3fbfe1ae94..866238e56e 100644 --- a/cmd/mirror/mirror.go +++ b/cmd/mirror/mirror.go @@ -56,7 +56,6 @@ Order of execution: copyEventstore(cmd.Context(), config) projections(cmd.Context(), projectionConfig, masterKey) - verifyMigration(cmd.Context(), config) }, } diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index c15747e74a..4e12b29748 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" @@ -84,6 +85,7 @@ type ProjectionsConfig struct { ExternalDomain string ExternalSecure bool InternalAuthZ internal_authz.Config + SystemAuthZ internal_authz.Config SystemDefaults systemdefaults.SystemDefaults Telemetry *handlers.TelemetryPusherConfig Login login.Config @@ -103,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) @@ -117,8 +120,11 @@ func projections( staticStorage, err := config.AssetStorage.NewStorage(client.DB) logging.OnError(err).Fatal("unable create static storage") - config.Eventstore.Querier = old_es.NewCRDB(client) - config.Eventstore.Pusher = new_es.NewEventstore(client) + newEventstore := new_es.NewEventstore(client) + config.Eventstore.Querier = old_es.NewPostgres(client) + config.Eventstore.Pusher = newEventstore + config.Eventstore.Searcher = newEventstore + es := eventstore.NewEventstore(config.Eventstore) esV4 := es_v4.NewEventstoreFromOne(es_v4_pg.New(client, &es_v4_pg.Config{ MaxRetries: config.Eventstore.MaxRetries, @@ -147,7 +153,7 @@ func projections( sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, 0, @@ -184,7 +190,7 @@ func projections( keys.Target, &http.Client{}, func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) }, sessionTokenVerifier, config.OIDC.DefaultAccessTokenLifetime, @@ -220,7 +226,6 @@ func projections( keys.SMS, keys.OIDC, config.OIDC.DefaultBackChannelLogoutLifetime, - client, nil, ) @@ -248,12 +253,14 @@ func projections( } }() - for i := 0; i < int(config.Projections.ConcurrentInstances); i++ { + for range int(config.Projections.ConcurrentInstances) { 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() @@ -265,42 +272,50 @@ 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) if err != nil { - logging.WithFields("instance", instance).OnError(err).Info("trigger failed") + logging.WithFields("instance", instance).WithError(err).Info("trigger failed") + failedInstances <- instance + continue + } + + err = projection.ProjectInstanceFields(ctx) + if err != nil { + logging.WithFields("instance", instance).WithError(err).Info("trigger fields failed") failedInstances <- instance continue } err = admin_handler.ProjectInstance(ctx) if err != nil { - logging.WithFields("instance", instance).OnError(err).Info("trigger admin handler failed") + logging.WithFields("instance", instance).WithError(err).Info("trigger admin handler failed") failedInstances <- instance continue } err = auth_handler.ProjectInstance(ctx) if err != nil { - logging.WithFields("instance", instance).OnError(err).Info("trigger auth handler failed") + logging.WithFields("instance", instance).WithError(err).Info("trigger auth handler failed") failedInstances <- instance continue } err = notification.ProjectInstance(ctx) if err != nil { - logging.WithFields("instance", instance).OnError(err).Info("trigger notification failed") + logging.WithFields("instance", instance).WithError(err).Info("trigger notification failed") failedInstances <- instance continue } + logging.WithFields("instance", instance).Info("projections done") } wg.Done() } -// 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/07.go b/cmd/setup/07.go index 73b9d3480b..590b220eb3 100644 --- a/cmd/setup/07.go +++ b/cmd/setup/07.go @@ -3,7 +3,7 @@ package setup import ( "context" "database/sql" - "embed" + _ "embed" "strings" "github.com/zitadel/zitadel/internal/eventstore" @@ -12,31 +12,20 @@ import ( var ( //go:embed 07/logstore.sql createLogstoreSchema07 string - //go:embed 07/cockroach/access.sql - //go:embed 07/postgres/access.sql - createAccessLogsTable07 embed.FS - //go:embed 07/cockroach/execution.sql - //go:embed 07/postgres/execution.sql - createExecutionLogsTable07 embed.FS + //go:embed 07/access.sql + createAccessLogsTable07 string + //go:embed 07/execution.sql + createExecutionLogsTable07 string ) type LogstoreTables struct { dbClient *sql.DB username string - dbType string } func (mig *LogstoreTables) Execute(ctx context.Context, _ eventstore.Event) error { - accessStmt, err := readStmt(createAccessLogsTable07, "07", mig.dbType, "access.sql") - if err != nil { - return err - } - executionStmt, err := readStmt(createExecutionLogsTable07, "07", mig.dbType, "execution.sql") - if err != nil { - return err - } - stmt := strings.ReplaceAll(createLogstoreSchema07, "%[1]s", mig.username) + accessStmt + executionStmt - _, err = mig.dbClient.ExecContext(ctx, stmt) + stmt := strings.ReplaceAll(createLogstoreSchema07, "%[1]s", mig.username) + createAccessLogsTable07 + createExecutionLogsTable07 + _, err := mig.dbClient.ExecContext(ctx, stmt) return err } diff --git a/cmd/setup/07/postgres/access.sql b/cmd/setup/07/access.sql similarity index 100% rename from cmd/setup/07/postgres/access.sql rename to cmd/setup/07/access.sql diff --git a/cmd/setup/07/cockroach/access.sql b/cmd/setup/07/cockroach/access.sql deleted file mode 100644 index fc5354cf32..0000000000 --- a/cmd/setup/07/cockroach/access.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE IF NOT EXISTS logstore.access ( - log_date TIMESTAMPTZ NOT NULL - , protocol INT NOT NULL - , request_url TEXT NOT NULL - , response_status INT NOT NULL - , request_headers JSONB - , response_headers JSONB - , instance_id TEXT NOT NULL - , project_id TEXT NOT NULL - , requested_domain TEXT - , requested_host TEXT - - , INDEX protocol_date_desc (instance_id, protocol, log_date DESC) STORING (request_url, response_status, request_headers) -); diff --git a/cmd/setup/07/cockroach/execution.sql b/cmd/setup/07/cockroach/execution.sql deleted file mode 100644 index b8e18b525a..0000000000 --- a/cmd/setup/07/cockroach/execution.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE IF NOT EXISTS logstore.execution ( - log_date TIMESTAMPTZ NOT NULL - , took INTERVAL - , message TEXT NOT NULL - , loglevel INT NOT NULL - , instance_id TEXT NOT NULL - , action_id TEXT NOT NULL - , metadata JSONB - - , INDEX log_date_desc (instance_id, log_date DESC) STORING (took) -); diff --git a/cmd/setup/07/postgres/execution.sql b/cmd/setup/07/execution.sql similarity index 100% rename from cmd/setup/07/postgres/execution.sql rename to cmd/setup/07/execution.sql diff --git a/cmd/setup/08.go b/cmd/setup/08.go index bec6a65ebb..fa006bd3cf 100644 --- a/cmd/setup/08.go +++ b/cmd/setup/08.go @@ -2,16 +2,15 @@ package setup import ( "context" - "embed" + _ "embed" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" ) var ( - //go:embed 08/cockroach/08.sql - //go:embed 08/postgres/08.sql - tokenIndexes08 embed.FS + //go:embed 08/08.sql + tokenIndexes08 string ) type AuthTokenIndexes struct { @@ -19,11 +18,7 @@ type AuthTokenIndexes struct { } func (mig *AuthTokenIndexes) Execute(ctx context.Context, _ eventstore.Event) error { - stmt, err := readStmt(tokenIndexes08, "08", mig.dbClient.Type(), "08.sql") - if err != nil { - return err - } - _, err = mig.dbClient.ExecContext(ctx, stmt) + _, err := mig.dbClient.ExecContext(ctx, tokenIndexes08) return err } diff --git a/cmd/setup/08/postgres/08.sql b/cmd/setup/08/08.sql similarity index 100% rename from cmd/setup/08/postgres/08.sql rename to cmd/setup/08/08.sql diff --git a/cmd/setup/08/cockroach/08.sql b/cmd/setup/08/cockroach/08.sql deleted file mode 100644 index aec4d54303..0000000000 --- a/cmd/setup/08/cockroach/08.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE INDEX IF NOT EXISTS inst_refresh_tkn_idx ON auth.tokens(instance_id, refresh_token_id); -CREATE INDEX IF NOT EXISTS inst_app_tkn_idx ON auth.tokens(instance_id, application_id); -CREATE INDEX IF NOT EXISTS inst_ro_tkn_idx ON auth.tokens(instance_id, resource_owner); -DROP INDEX IF EXISTS auth.tokens@user_user_agent_idx; -CREATE INDEX IF NOT EXISTS inst_usr_agnt_tkn_idx ON auth.tokens(instance_id, user_id, user_agent_id); \ No newline at end of file diff --git a/cmd/setup/10.go b/cmd/setup/10.go index 93c017305c..b134fcab62 100644 --- a/cmd/setup/10.go +++ b/cmd/setup/10.go @@ -3,7 +3,7 @@ package setup import ( "context" "database/sql" - "embed" + _ "embed" "time" "github.com/cockroachdb/cockroach-go/v2/crdb" @@ -18,9 +18,8 @@ var ( correctCreationDate10CreateTable string //go:embed 10/10_fill_table.sql correctCreationDate10FillTable string - //go:embed 10/cockroach/10_update.sql - //go:embed 10/postgres/10_update.sql - correctCreationDate10Update embed.FS + //go:embed 10/10_update.sql + correctCreationDate10Update string //go:embed 10/10_count_wrong_events.sql correctCreationDate10CountWrongEvents string //go:embed 10/10_empty_table.sql @@ -40,11 +39,6 @@ func (mig *CorrectCreationDate) Execute(ctx context.Context, _ eventstore.Event) logging.WithFields("mig", mig.String(), "iteration", i).Debug("start iteration") var affected int64 err = crdb.ExecuteTx(ctx, mig.dbClient.DB, nil, func(tx *sql.Tx) error { - if mig.dbClient.Type() == "cockroach" { - if _, err := tx.Exec("SET experimental_enable_temp_tables=on"); err != nil { - return err - } - } _, err := tx.ExecContext(ctx, correctCreationDate10CreateTable) if err != nil { return err @@ -66,11 +60,7 @@ func (mig *CorrectCreationDate) Execute(ctx context.Context, _ eventstore.Event) return err } - updateStmt, err := readStmt(correctCreationDate10Update, "10", mig.dbClient.Type(), "10_update.sql") - if err != nil { - return err - } - _, err = tx.ExecContext(ctx, updateStmt) + _, err = tx.ExecContext(ctx, correctCreationDate10Update) if err != nil { return err } diff --git a/cmd/setup/10/postgres/10_update.sql b/cmd/setup/10/10_update.sql similarity index 100% rename from cmd/setup/10/postgres/10_update.sql rename to cmd/setup/10/10_update.sql diff --git a/cmd/setup/10/cockroach/10_update.sql b/cmd/setup/10/cockroach/10_update.sql deleted file mode 100644 index 9e7d7f993a..0000000000 --- a/cmd/setup/10/cockroach/10_update.sql +++ /dev/null @@ -1 +0,0 @@ -UPDATE eventstore.events e SET (creation_date, "position") = (we.next_cd, we.next_cd::DECIMAL) FROM wrong_events we WHERE e.event_sequence = we.event_sequence AND e.instance_id = we.instance_id; diff --git a/cmd/setup/14.go b/cmd/setup/14.go index f0ea1b819a..2cd5ac2c57 100644 --- a/cmd/setup/14.go +++ b/cmd/setup/14.go @@ -15,8 +15,7 @@ import ( ) var ( - //go:embed 14/cockroach/*.sql - //go:embed 14/postgres/*.sql + //go:embed 14/*.sql newEventsTable embed.FS ) @@ -40,7 +39,7 @@ func (mig *NewEventsTable) Execute(ctx context.Context, _ eventstore.Event) erro return err } - statements, err := readStatements(newEventsTable, "14", mig.dbClient.Type()) + statements, err := readStatements(newEventsTable, "14") if err != nil { return err } diff --git a/cmd/setup/14/cockroach/01_disable_inserts.sql b/cmd/setup/14/01_disable_inserts.sql similarity index 100% rename from cmd/setup/14/cockroach/01_disable_inserts.sql rename to cmd/setup/14/01_disable_inserts.sql diff --git a/cmd/setup/14/postgres/02_create_and_fill_events2.sql b/cmd/setup/14/02_create_and_fill_events2.sql similarity index 100% rename from cmd/setup/14/postgres/02_create_and_fill_events2.sql rename to cmd/setup/14/02_create_and_fill_events2.sql diff --git a/cmd/setup/14/postgres/03_events2_pk.sql b/cmd/setup/14/03_events2_pk.sql similarity index 100% rename from cmd/setup/14/postgres/03_events2_pk.sql rename to cmd/setup/14/03_events2_pk.sql diff --git a/cmd/setup/14/postgres/04_constraints.sql b/cmd/setup/14/04_constraints.sql similarity index 100% rename from cmd/setup/14/postgres/04_constraints.sql rename to cmd/setup/14/04_constraints.sql diff --git a/cmd/setup/14/postgres/05_indexes.sql b/cmd/setup/14/05_indexes.sql similarity index 100% rename from cmd/setup/14/postgres/05_indexes.sql rename to cmd/setup/14/05_indexes.sql diff --git a/cmd/setup/14/cockroach/02_create_and_fill_events2.sql b/cmd/setup/14/cockroach/02_create_and_fill_events2.sql deleted file mode 100644 index 300ac4b621..0000000000 --- a/cmd/setup/14/cockroach/02_create_and_fill_events2.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE TABLE eventstore.events2 ( - instance_id, - aggregate_type, - aggregate_id, - - event_type, - "sequence", - revision, - created_at, - payload, - creator, - "owner", - - "position", - in_tx_order, - - PRIMARY KEY (instance_id, aggregate_type, aggregate_id, "sequence") -) AS SELECT - instance_id, - aggregate_type, - aggregate_id, - - event_type, - event_sequence, - substr(aggregate_version, 2)::SMALLINT, - creation_date, - event_data, - editor_user, - resource_owner, - - creation_date::DECIMAL, - event_sequence -FROM eventstore.events_old; \ No newline at end of file diff --git a/cmd/setup/14/cockroach/03_constraints.sql b/cmd/setup/14/cockroach/03_constraints.sql deleted file mode 100644 index 62f119cc43..0000000000 --- a/cmd/setup/14/cockroach/03_constraints.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE eventstore.events2 ALTER COLUMN event_type SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN revision SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN creator SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN "owner" SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN "position" SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN in_tx_order SET NOT NULL; \ No newline at end of file diff --git a/cmd/setup/14/cockroach/04_indexes.sql b/cmd/setup/14/cockroach/04_indexes.sql deleted file mode 100644 index a442653606..0000000000 --- a/cmd/setup/14/cockroach/04_indexes.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE INDEX IF NOT EXISTS es_active_instances ON eventstore.events2 (created_at DESC) STORING ("position"); -CREATE INDEX IF NOT EXISTS es_wm ON eventstore.events2 (aggregate_id, instance_id, aggregate_type, event_type); -CREATE INDEX IF NOT EXISTS es_projection ON eventstore.events2 (instance_id, aggregate_type, event_type, "position"); \ No newline at end of file diff --git a/cmd/setup/14/postgres/01_disable_inserts.sql b/cmd/setup/14/postgres/01_disable_inserts.sql deleted file mode 100644 index 0f3c277eba..0000000000 --- a/cmd/setup/14/postgres/01_disable_inserts.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE eventstore.events RENAME TO events_old; \ No newline at end of file diff --git a/cmd/setup/15.go b/cmd/setup/15.go index 2e75ffb118..54161ddef9 100644 --- a/cmd/setup/15.go +++ b/cmd/setup/15.go @@ -11,8 +11,7 @@ import ( ) var ( - //go:embed 15/cockroach/*.sql - //go:embed 15/postgres/*.sql + //go:embed 15/*.sql currentProjectionState embed.FS ) @@ -21,7 +20,7 @@ type CurrentProjectionState struct { } func (mig *CurrentProjectionState) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(currentProjectionState, "15", mig.dbClient.Type()) + statements, err := readStatements(currentProjectionState, "15") if err != nil { return err } diff --git a/cmd/setup/15/cockroach/01_new_failed_events.sql b/cmd/setup/15/01_new_failed_events.sql similarity index 100% rename from cmd/setup/15/cockroach/01_new_failed_events.sql rename to cmd/setup/15/01_new_failed_events.sql diff --git a/cmd/setup/15/postgres/02_fe_from_projections.sql b/cmd/setup/15/02_fe_from_projections.sql similarity index 100% rename from cmd/setup/15/postgres/02_fe_from_projections.sql rename to cmd/setup/15/02_fe_from_projections.sql diff --git a/cmd/setup/15/cockroach/03_fe_from_adminapi.sql b/cmd/setup/15/03_fe_from_adminapi.sql similarity index 100% rename from cmd/setup/15/cockroach/03_fe_from_adminapi.sql rename to cmd/setup/15/03_fe_from_adminapi.sql diff --git a/cmd/setup/15/cockroach/04_fe_from_auth.sql b/cmd/setup/15/04_fe_from_auth.sql similarity index 100% rename from cmd/setup/15/cockroach/04_fe_from_auth.sql rename to cmd/setup/15/04_fe_from_auth.sql diff --git a/cmd/setup/15/cockroach/05_current_states.sql b/cmd/setup/15/05_current_states.sql similarity index 100% rename from cmd/setup/15/cockroach/05_current_states.sql rename to cmd/setup/15/05_current_states.sql diff --git a/cmd/setup/15/postgres/06_cs_from_projections.sql b/cmd/setup/15/06_cs_from_projections.sql similarity index 100% rename from cmd/setup/15/postgres/06_cs_from_projections.sql rename to cmd/setup/15/06_cs_from_projections.sql diff --git a/cmd/setup/15/postgres/07_cs_from_adminapi.sql b/cmd/setup/15/07_cs_from_adminapi.sql similarity index 100% rename from cmd/setup/15/postgres/07_cs_from_adminapi.sql rename to cmd/setup/15/07_cs_from_adminapi.sql diff --git a/cmd/setup/15/postgres/08_cs_from_auth.sql b/cmd/setup/15/08_cs_from_auth.sql similarity index 100% rename from cmd/setup/15/postgres/08_cs_from_auth.sql rename to cmd/setup/15/08_cs_from_auth.sql diff --git a/cmd/setup/15/cockroach/02_fe_from_projections.sql b/cmd/setup/15/cockroach/02_fe_from_projections.sql deleted file mode 100644 index 8bf7a4b8d4..0000000000 --- a/cmd/setup/15/cockroach/02_fe_from_projections.sql +++ /dev/null @@ -1,26 +0,0 @@ -INSERT INTO projections.failed_events2 ( - projection_name - , instance_id - , aggregate_type - , aggregate_id - , event_creation_date - , failed_sequence - , failure_count - , error - , last_failed -) SELECT - fe.projection_name - , fe.instance_id - , e.aggregate_type - , e.aggregate_id - , e.created_at - , e.sequence - , fe.failure_count - , fe.error - , fe.last_failed -FROM - projections.failed_events fe -JOIN eventstore.events2 e ON - e.instance_id = fe.instance_id - AND e.sequence = fe.failed_sequence -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/cockroach/06_cs_from_projections.sql b/cmd/setup/15/cockroach/06_cs_from_projections.sql deleted file mode 100644 index 579afb6d4c..0000000000 --- a/cmd/setup/15/cockroach/06_cs_from_projections.sql +++ /dev/null @@ -1,29 +0,0 @@ -INSERT INTO projections.current_states ( - projection_name - , instance_id - , event_date - , "position" - , last_updated -) (SELECT - cs.projection_name - , cs.instance_id - , e.created_at - , e.position - , cs.timestamp -FROM - projections.current_sequences cs -JOIN eventstore.events2 e ON - e.instance_id = cs.instance_id - AND e.aggregate_type = cs.aggregate_type - AND e.sequence = cs.current_sequence - AND cs.current_sequence = ( - SELECT - MAX(cs2.current_sequence) - FROM - projections.current_sequences cs2 - WHERE - cs.projection_name = cs2.projection_name - AND cs.instance_id = cs2.instance_id - ) -) -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/cockroach/07_cs_from_adminapi.sql b/cmd/setup/15/cockroach/07_cs_from_adminapi.sql deleted file mode 100644 index c40d13a067..0000000000 --- a/cmd/setup/15/cockroach/07_cs_from_adminapi.sql +++ /dev/null @@ -1,28 +0,0 @@ -INSERT INTO projections.current_states ( - projection_name - , instance_id - , event_date - , "position" - , last_updated -) (SELECT - cs.view_name - , cs.instance_id - , e.created_at - , e.position - , cs.last_successful_spooler_run -FROM - adminapi.current_sequences cs -JOIN eventstore.events2 e ON - e.instance_id = cs.instance_id - AND e.sequence = cs.current_sequence - AND cs.current_sequence = ( - SELECT - MAX(cs2.current_sequence) - FROM - adminapi.current_sequences cs2 - WHERE - cs.view_name = cs2.view_name - AND cs.instance_id = cs2.instance_id - ) -) -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/cockroach/08_cs_from_auth.sql b/cmd/setup/15/cockroach/08_cs_from_auth.sql deleted file mode 100644 index c8e7236107..0000000000 --- a/cmd/setup/15/cockroach/08_cs_from_auth.sql +++ /dev/null @@ -1,28 +0,0 @@ -INSERT INTO projections.current_states ( - projection_name - , instance_id - , event_date - , "position" - , last_updated -) (SELECT - cs.view_name - , cs.instance_id - , e.created_at - , e.position - , cs.last_successful_spooler_run -FROM - auth.current_sequences cs -JOIN eventstore.events2 e ON - e.instance_id = cs.instance_id - AND e.sequence = cs.current_sequence - AND cs.current_sequence = ( - SELECT - MAX(cs2.current_sequence) - FROM - auth.current_sequences cs2 - WHERE - cs.view_name = cs2.view_name - AND cs.instance_id = cs2.instance_id - ) -) -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/postgres/01_new_failed_events.sql b/cmd/setup/15/postgres/01_new_failed_events.sql deleted file mode 100644 index 5fa39c08a5..0000000000 --- a/cmd/setup/15/postgres/01_new_failed_events.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS projections.failed_events2 ( - projection_name TEXT NOT NULL - , instance_id TEXT NOT NULL - - , aggregate_type TEXT NOT NULL - , aggregate_id TEXT NOT NULL - , event_creation_date TIMESTAMPTZ NOT NULL - , failed_sequence INT8 NOT NULL - - , failure_count INT2 NULL DEFAULT 0 - , error TEXT - , last_failed TIMESTAMPTZ - - , PRIMARY KEY (projection_name, instance_id, aggregate_type, aggregate_id, failed_sequence) -); -CREATE INDEX IF NOT EXISTS fe2_instance_id_idx on projections.failed_events2 (instance_id); \ No newline at end of file diff --git a/cmd/setup/15/postgres/03_fe_from_adminapi.sql b/cmd/setup/15/postgres/03_fe_from_adminapi.sql deleted file mode 100644 index 1616662fed..0000000000 --- a/cmd/setup/15/postgres/03_fe_from_adminapi.sql +++ /dev/null @@ -1,26 +0,0 @@ -INSERT INTO projections.failed_events2 ( - projection_name - , instance_id - , aggregate_type - , aggregate_id - , event_creation_date - , failed_sequence - , failure_count - , error - , last_failed -) SELECT - fe.view_name - , fe.instance_id - , e.aggregate_type - , e.aggregate_id - , e.created_at - , e.sequence - , fe.failure_count - , fe.err_msg - , fe.last_failed -FROM - adminapi.failed_events fe -JOIN eventstore.events2 e ON - e.instance_id = fe.instance_id - AND e.sequence = fe.failed_sequence -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/postgres/04_fe_from_auth.sql b/cmd/setup/15/postgres/04_fe_from_auth.sql deleted file mode 100644 index a249293e24..0000000000 --- a/cmd/setup/15/postgres/04_fe_from_auth.sql +++ /dev/null @@ -1,26 +0,0 @@ -INSERT INTO projections.failed_events2 ( - projection_name - , instance_id - , aggregate_type - , aggregate_id - , event_creation_date - , failed_sequence - , failure_count - , error - , last_failed -) SELECT - fe.view_name - , fe.instance_id - , e.aggregate_type - , e.aggregate_id - , e.created_at - , e.sequence - , fe.failure_count - , fe.err_msg - , fe.last_failed -FROM - auth.failed_events fe -JOIN eventstore.events2 e ON - e.instance_id = fe.instance_id - AND e.sequence = fe.failed_sequence -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/postgres/05_current_states.sql b/cmd/setup/15/postgres/05_current_states.sql deleted file mode 100644 index bc2f5ed771..0000000000 --- a/cmd/setup/15/postgres/05_current_states.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE IF NOT EXISTS projections.current_states ( - projection_name TEXT NOT NULL - , instance_id TEXT NOT NULL - - , last_updated TIMESTAMPTZ - - , aggregate_id TEXT - , aggregate_type TEXT - , "sequence" INT8 - , event_date TIMESTAMPTZ - , "position" DECIMAL - - , PRIMARY KEY (projection_name, instance_id) -); -CREATE INDEX IF NOT EXISTS cs_instance_id_idx ON projections.current_states (instance_id); \ No newline at end of file diff --git a/cmd/setup/34.go b/cmd/setup/34.go index 59854e9e97..75e4076803 100644 --- a/cmd/setup/34.go +++ b/cmd/setup/34.go @@ -3,17 +3,14 @@ package setup import ( "context" _ "embed" - "fmt" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" ) var ( - //go:embed 34/cockroach/34_cache_schema.sql - addCacheSchemaCockroach string - //go:embed 34/postgres/34_cache_schema.sql - addCacheSchemaPostgres string + //go:embed 34/34_cache_schema.sql + addCacheSchema string ) type AddCacheSchema struct { @@ -21,14 +18,7 @@ type AddCacheSchema struct { } func (mig *AddCacheSchema) Execute(ctx context.Context, _ eventstore.Event) (err error) { - switch mig.dbClient.Type() { - case "cockroach": - _, err = mig.dbClient.ExecContext(ctx, addCacheSchemaCockroach) - case "postgres": - _, err = mig.dbClient.ExecContext(ctx, addCacheSchemaPostgres) - default: - err = fmt.Errorf("add cache schema: unsupported db type %q", mig.dbClient.Type()) - } + _, err = mig.dbClient.ExecContext(ctx, addCacheSchema) return err } diff --git a/cmd/setup/34/postgres/34_cache_schema.sql b/cmd/setup/34/34_cache_schema.sql similarity index 100% rename from cmd/setup/34/postgres/34_cache_schema.sql rename to cmd/setup/34/34_cache_schema.sql diff --git a/cmd/setup/34/cockroach/34_cache_schema.sql b/cmd/setup/34/cockroach/34_cache_schema.sql deleted file mode 100644 index 0f866b0ccd..0000000000 --- a/cmd/setup/34/cockroach/34_cache_schema.sql +++ /dev/null @@ -1,27 +0,0 @@ -create schema if not exists cache; - -create table if not exists cache.objects ( - cache_name varchar not null, - id uuid not null default gen_random_uuid(), - created_at timestamptz not null default now(), - last_used_at timestamptz not null default now(), - payload jsonb not null, - - primary key(cache_name, id) -); - -create table if not exists cache.string_keys( - cache_name varchar not null check (cache_name <> ''), - index_id integer not null check (index_id > 0), - index_key varchar not null check (index_key <> ''), - object_id uuid not null, - - primary key (cache_name, index_id, index_key), - constraint fk_object - foreign key(cache_name, object_id) - references cache.objects(cache_name, id) - on delete cascade -); - -create index if not exists string_keys_object_id_idx - on cache.string_keys (cache_name, object_id); -- for delete cascade diff --git a/cmd/setup/35.go b/cmd/setup/35.go index f8473cfbfd..68e08bdfdb 100644 --- a/cmd/setup/35.go +++ b/cmd/setup/35.go @@ -21,7 +21,7 @@ type AddPositionToIndexEsWm struct { } func (mig *AddPositionToIndexEsWm) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(addPositionToEsWmIndex, "35", "") + statements, err := readStatements(addPositionToEsWmIndex, "35") if err != nil { return err } diff --git a/cmd/setup/40.go b/cmd/setup/40.go index b16b9226f7..86cdab0d11 100644 --- a/cmd/setup/40.go +++ b/cmd/setup/40.go @@ -24,8 +24,7 @@ const ( ) var ( - //go:embed 40/cockroach/*.sql - //go:embed 40/postgres/*.sql + //go:embed 40/*.sql initPushFunc embed.FS ) @@ -112,5 +111,5 @@ func (mig *InitPushFunc) inTxOrderType(ctx context.Context) (typeName string, er } func (mig *InitPushFunc) filePath(fileName string) string { - return path.Join("40", mig.dbClient.Type(), fileName) + return path.Join("40", fileName) } diff --git a/cmd/setup/40/cockroach/00_in_tx_order_type.sql b/cmd/setup/40/00_in_tx_order_type.sql similarity index 100% rename from cmd/setup/40/cockroach/00_in_tx_order_type.sql rename to cmd/setup/40/00_in_tx_order_type.sql diff --git a/cmd/setup/40/postgres/01_type.sql b/cmd/setup/40/01_type.sql similarity index 100% rename from cmd/setup/40/postgres/01_type.sql rename to cmd/setup/40/01_type.sql diff --git a/cmd/setup/40/postgres/02_func.sql b/cmd/setup/40/02_func.sql similarity index 100% rename from cmd/setup/40/postgres/02_func.sql rename to cmd/setup/40/02_func.sql diff --git a/cmd/setup/40/cockroach/01_type.sql b/cmd/setup/40/cockroach/01_type.sql deleted file mode 100644 index e26af2f828..0000000000 --- a/cmd/setup/40/cockroach/01_type.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TYPE IF NOT EXISTS eventstore.command AS ( - instance_id TEXT - , aggregate_type TEXT - , aggregate_id TEXT - , command_type TEXT - , revision INT2 - , payload JSONB - , creator TEXT - , owner TEXT -); diff --git a/cmd/setup/40/cockroach/02_func.sql b/cmd/setup/40/cockroach/02_func.sql deleted file mode 100644 index 9cb45529ad..0000000000 --- a/cmd/setup/40/cockroach/02_func.sql +++ /dev/null @@ -1,137 +0,0 @@ -CREATE OR REPLACE FUNCTION eventstore.latest_aggregate_state( - instance_id TEXT - , aggregate_type TEXT - , aggregate_id TEXT - - , sequence OUT BIGINT - , owner OUT TEXT -) - LANGUAGE 'plpgsql' -AS $$ - BEGIN - SELECT - COALESCE(e.sequence, 0) AS sequence - , e.owner - INTO - sequence - , owner - FROM - eventstore.events2 e - WHERE - e.instance_id = $1 - AND e.aggregate_type = $2 - AND e.aggregate_id = $3 - ORDER BY - e.sequence DESC - LIMIT 1; - - RETURN; - END; -$$; - -CREATE OR REPLACE FUNCTION eventstore.commands_to_events2(commands eventstore.command[]) - RETURNS eventstore.events2[] - LANGUAGE 'plpgsql' -AS $$ -DECLARE - current_sequence BIGINT; - current_owner TEXT; - - instance_id TEXT; - aggregate_type TEXT; - aggregate_id TEXT; - - _events eventstore.events2[]; - - _aggregates CURSOR FOR - select - DISTINCT ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - FROM - UNNEST(commands) AS c; -BEGIN - OPEN _aggregates; - LOOP - FETCH NEXT IN _aggregates INTO instance_id, aggregate_type, aggregate_id; - -- crdb does not support EXIT WHEN NOT FOUND - EXIT WHEN instance_id IS NULL; - - SELECT - * - INTO - current_sequence - , current_owner - FROM eventstore.latest_aggregate_state( - instance_id - , aggregate_type - , aggregate_id - ); - - -- RETURN QUERY is not supported by crdb: https://github.com/cockroachdb/cockroach/issues/105240 - SELECT - ARRAY_CAT(_events, ARRAY_AGG(e)) - INTO - _events - FROM ( - SELECT - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - , ("c").command_type -- AS event_type - , COALESCE(current_sequence, 0) + ROW_NUMBER() OVER () -- AS sequence - , ("c").revision - , NOW() -- AS created_at - , ("c").payload - , ("c").creator - , COALESCE(current_owner, ("c").owner) -- AS owner - , cluster_logical_timestamp() -- AS position - , ordinality::{{ .InTxOrderType }} -- AS in_tx_order - FROM - UNNEST(commands) WITH ORDINALITY AS c - WHERE - ("c").instance_id = instance_id - AND ("c").aggregate_type = aggregate_type - AND ("c").aggregate_id = aggregate_id - ) AS e; - END LOOP; - CLOSE _aggregates; - RETURN _events; -END; -$$; - -CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command[]) RETURNS SETOF eventstore.events2 AS $$ - INSERT INTO eventstore.events2 - SELECT - ("e").instance_id - , ("e").aggregate_type - , ("e").aggregate_id - , ("e").event_type - , ("e").sequence - , ("e").revision - , ("e").created_at - , ("e").payload - , ("e").creator - , ("e").owner - , ("e")."position" - , ("e").in_tx_order - FROM - UNNEST(eventstore.commands_to_events2(commands)) e - ORDER BY - in_tx_order - RETURNING * -$$ LANGUAGE SQL; - -/* -select (c).* from UNNEST(eventstore.commands_to_events2( -ARRAY[ - ROW('', 'system', 'SYSTEM', 'ct1', 1, '{"key": "value"}', 'c1', 'SYSTEM') - , ROW('', 'system', 'SYSTEM', 'ct2', 1, '{"key": "value"}', 'c1', 'SYSTEM') - , ROW('289525561255060732', 'org', '289575074711790844', 'ct3', 1, '{"key": "value"}', 'c1', '289575074711790844') - , ROW('289525561255060732', 'user', '289575075164906748', 'ct3', 1, '{"key": "value"}', 'c1', '289575074711790844') - , ROW('289525561255060732', 'oidc_session', 'V2_289575178579535100', 'ct3', 1, '{"key": "value"}', 'c1', '289575074711790844') - , ROW('', 'system', 'SYSTEM', 'ct3', 1, '{"key": "value"}', 'c1', 'SYSTEM') -]::eventstore.command[] -) )c; -*/ - diff --git a/cmd/setup/40/postgres/00_in_tx_order_type.sql b/cmd/setup/40/postgres/00_in_tx_order_type.sql deleted file mode 100644 index 68b7daf984..0000000000 --- a/cmd/setup/40/postgres/00_in_tx_order_type.sql +++ /dev/null @@ -1,5 +0,0 @@ -SELECT data_type -FROM information_schema.columns -WHERE table_schema = 'eventstore' -AND table_name = 'events2' -AND column_name = 'in_tx_order'; diff --git a/cmd/setup/43.go b/cmd/setup/43.go index 844c25cf24..1fa09773bc 100644 --- a/cmd/setup/43.go +++ b/cmd/setup/43.go @@ -12,8 +12,7 @@ import ( ) var ( - //go:embed 43/cockroach/*.sql - //go:embed 43/postgres/*.sql + //go:embed 43/*.sql createFieldsDomainIndex embed.FS ) @@ -22,7 +21,7 @@ type CreateFieldsDomainIndex struct { } func (mig *CreateFieldsDomainIndex) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(createFieldsDomainIndex, "43", mig.dbClient.Type()) + statements, err := readStatements(createFieldsDomainIndex, "43") if err != nil { return err } diff --git a/cmd/setup/43/postgres/43.sql b/cmd/setup/43/43.sql similarity index 100% rename from cmd/setup/43/postgres/43.sql rename to cmd/setup/43/43.sql diff --git a/cmd/setup/43/cockroach/43.sql b/cmd/setup/43/cockroach/43.sql deleted file mode 100644 index 9152130970..0000000000 --- a/cmd/setup/43/cockroach/43.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE INDEX CONCURRENTLY IF NOT EXISTS fields_instance_domains_idx -ON eventstore.fields (object_id) -WHERE object_type = 'instance_domain' AND field_name = 'domain'; \ No newline at end of file diff --git a/cmd/setup/44.go b/cmd/setup/44.go index 11c355a053..5eb2f8d5c1 100644 --- a/cmd/setup/44.go +++ b/cmd/setup/44.go @@ -21,7 +21,7 @@ type ReplaceCurrentSequencesIndex struct { } func (mig *ReplaceCurrentSequencesIndex) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(replaceCurrentSequencesIndex, "44", "") + statements, err := readStatements(replaceCurrentSequencesIndex, "44") if err != nil { return err } diff --git a/cmd/setup/46.go b/cmd/setup/46.go index e48b16e4b0..3593a1b668 100644 --- a/cmd/setup/46.go +++ b/cmd/setup/46.go @@ -21,7 +21,7 @@ var ( ) func (mig *InitPermissionFunctions) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(permissionFunctions, "46", "") + statements, err := readStatements(permissionFunctions, "46") if err != nil { return err } diff --git a/cmd/setup/49.go b/cmd/setup/49.go index 28bf797110..8465589140 100644 --- a/cmd/setup/49.go +++ b/cmd/setup/49.go @@ -21,7 +21,7 @@ var ( ) func (mig *InitPermittedOrgsFunction) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(permittedOrgsFunction, "49", "") + statements, err := readStatements(permittedOrgsFunction, "49") if err != nil { return err } diff --git a/cmd/setup/53.go b/cmd/setup/53.go new file mode 100644 index 0000000000..83a7b1c0e2 --- /dev/null +++ b/cmd/setup/53.go @@ -0,0 +1,37 @@ +package setup + +import ( + "context" + "embed" + "fmt" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +type InitPermittedOrgsFunction53 struct { + dbClient *database.DB +} + +//go:embed 53/*.sql +var permittedOrgsFunction53 embed.FS + +func (mig *InitPermittedOrgsFunction53) Execute(ctx context.Context, _ eventstore.Event) error { + statements, err := readStatements(permittedOrgsFunction53, "53") + if err != nil { + return err + } + for _, stmt := range statements { + logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement") + if _, err := mig.dbClient.ExecContext(ctx, stmt.query); err != nil { + return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err) + } + } + return nil +} + +func (*InitPermittedOrgsFunction53) String() string { + return "53_init_permitted_orgs_function" +} diff --git a/cmd/setup/53/01-get-permissions-from-JSON.sql b/cmd/setup/53/01-get-permissions-from-JSON.sql new file mode 100644 index 0000000000..b6415fa180 --- /dev/null +++ b/cmd/setup/53/01-get-permissions-from-JSON.sql @@ -0,0 +1,43 @@ +DROP FUNCTION IF EXISTS eventstore.get_system_permissions; + +CREATE OR REPLACE FUNCTION eventstore.get_system_permissions( + permissions_json JSONB + /* + [ + { + "member_type": "System", + "aggregate_id": "", + "object_id": "", + "permissions": ["iam.read", "iam.write", "iam.polic.read"] + }, + { + "member_type": "IAM", + "aggregate_id": "310716990375453665", + "object_id": "", + "permissions": ["iam.read", "iam.write", "iam.polic.read"] + } + ] + */ + , permm TEXT +) +RETURNS TABLE ( + member_type TEXT, + aggregate_id TEXT, + object_id TEXT +) + LANGUAGE 'plpgsql' +AS $$ +BEGIN + RETURN QUERY + SELECT res.member_type, res.aggregate_id, res.object_id FROM ( + SELECT + (perm)->>'member_type' AS member_type, + (perm)->>'aggregate_id' AS aggregate_id, + (perm)->>'object_id' AS object_id, + permission + FROM jsonb_array_elements(permissions_json) AS perm + CROSS JOIN jsonb_array_elements_text(perm->'permissions') AS permission) AS res + WHERE res. permission= permm; +END; +$$; + diff --git a/cmd/setup/53/02-permitted_orgs_function.sql b/cmd/setup/53/02-permitted_orgs_function.sql new file mode 100644 index 0000000000..b6f61c6225 --- /dev/null +++ b/cmd/setup/53/02-permitted_orgs_function.sql @@ -0,0 +1,144 @@ +DROP FUNCTION IF EXISTS eventstore.check_system_user_perms; + +CREATE OR REPLACE FUNCTION eventstore.check_system_user_perms( + system_user_perms JSONB + , perm TEXT + , filter_orgs TEXT + + , org_ids OUT TEXT[] +) + LANGUAGE 'plpgsql' +AS $$ +BEGIN + + WITH found_permissions(member_type, aggregate_id, object_id ) AS ( + SELECT * FROM eventstore.get_system_permissions( + system_user_perms, + perm) + ) + + SELECT array_agg(DISTINCT o.org_id) INTO org_ids + FROM eventstore.instance_orgs o, found_permissions + WHERE + CASE WHEN (SELECT TRUE WHERE found_permissions.member_type = 'System' LIMIT 1) THEN + TRUE + WHEN (SELECT TRUE WHERE found_permissions.member_type = 'IAM' LIMIT 1) THEN + -- aggregate_id not present + CASE WHEN (SELECT TRUE WHERE '' = ANY ( + ( + SELECT array_agg(found_permissions.aggregate_id) + FROM found_permissions + WHERE member_type = 'IAM' + GROUP BY member_type + LIMIT 1 + )::TEXT[])) THEN + TRUE + -- aggregate_id is present + ELSE + o.instance_id = ANY ( + ( + SELECT array_agg(found_permissions.aggregate_id) + FROM found_permissions + WHERE member_type = 'IAM' + GROUP BY member_type + LIMIT 1 + )::TEXT[]) + END + WHEN (SELECT TRUE WHERE found_permissions.member_type = 'Organization' LIMIT 1) THEN + -- aggregate_id not present + CASE WHEN (SELECT TRUE WHERE '' = ANY ( + ( + SELECT array_agg(found_permissions.aggregate_id) + FROM found_permissions + WHERE member_type = 'Organization' + GROUP BY member_type + LIMIT 1 + )::TEXT[])) THEN + TRUE + -- aggregate_id is present + ELSE + o.org_id = ANY ( + ( + SELECT array_agg(found_permissions.aggregate_id) + FROM found_permissions + WHERE member_type = 'Organization' + GROUP BY member_type + LIMIT 1 + )::TEXT[]) + END + END + AND + CASE WHEN filter_orgs != '' + THEN o.org_id IN (filter_orgs) + ELSE TRUE END + LIMIT 1; +END; +$$; + + +DROP FUNCTION IF EXISTS eventstore.permitted_orgs; + +CREATE OR REPLACE FUNCTION eventstore.permitted_orgs( + instanceId TEXT + , userId TEXT + , system_user_perms JSONB + , perm TEXT + , filter_orgs TEXT + + , org_ids OUT TEXT[] +) + LANGUAGE 'plpgsql' +AS $$ +BEGIN + + -- if system user + IF system_user_perms IS NOT NULL THEN + org_ids := eventstore.check_system_user_perms(system_user_perms, perm, filter_orgs); + -- if human/machine user + ELSE + DECLARE + matched_roles TEXT[]; -- roles containing permission + BEGIN + + SELECT array_agg(rp.role) INTO matched_roles + FROM eventstore.role_permissions rp + WHERE rp.instance_id = instanceId + AND rp.permission = perm; + + -- First try if the permission was granted thru an instance-level role + DECLARE + has_instance_permission bool; + BEGIN + SELECT true INTO has_instance_permission + FROM eventstore.instance_members im + WHERE im.role = ANY(matched_roles) + AND im.instance_id = instanceId + AND im.user_id = userId + LIMIT 1; + + IF has_instance_permission THEN + -- Return all organizations or only those in filter_orgs + SELECT array_agg(o.org_id) INTO org_ids + FROM eventstore.instance_orgs o + WHERE o.instance_id = instanceId + AND CASE WHEN filter_orgs != '' + THEN o.org_id IN (filter_orgs) + ELSE TRUE END; + RETURN; + END IF; + END; + + -- Return the organizations where permission were granted thru org-level roles + SELECT array_agg(sub.org_id) INTO org_ids + FROM ( + SELECT DISTINCT om.org_id + FROM eventstore.org_members om + WHERE om.role = ANY(matched_roles) + AND om.instance_id = instanceID + AND om.user_id = userId + ) AS sub; + END; + END IF; +END; +$$; + diff --git a/cmd/setup/cleanup.go b/cmd/setup/cleanup.go index c162806b14..69f7c72e53 100644 --- a/cmd/setup/cleanup.go +++ b/cmd/setup/cleanup.go @@ -33,7 +33,7 @@ func Cleanup(ctx context.Context, config *Config) { logging.OnError(err).Fatal("unable to connect to database") config.Eventstore.Pusher = new_es.NewEventstore(dbClient) - config.Eventstore.Querier = old_es.NewCRDB(dbClient) + config.Eventstore.Querier = old_es.NewPostgres(dbClient) es := eventstore.NewEventstore(config.Eventstore) step, err := migration.LastStuckStep(ctx, es) diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 2a94b7919e..4742b94c7b 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -12,7 +12,7 @@ import ( "github.com/zitadel/zitadel/cmd/encryption" "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" - internal_authz "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/cache/connector" @@ -22,6 +22,7 @@ import ( "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/notification/handlers" "github.com/zitadel/zitadel/internal/query/projection" @@ -34,7 +35,8 @@ type Config struct { Database database.Config Caches *connector.CachesConfig SystemDefaults systemdefaults.SystemDefaults - InternalAuthZ internal_authz.Config + InternalAuthZ authz.Config + SystemAuthZ authz.Config ExternalDomain string ExternalPort uint16 ExternalSecure bool @@ -45,6 +47,7 @@ type Config struct { Machine *id.Config Projections projection.Config Notifications handlers.WorkerConfig + Executions execution.WorkerConfig Eventstore *eventstore.Config InitProjections InitProjections @@ -53,7 +56,7 @@ type Config struct { Login login.Config WebAuthNName string Telemetry *handlers.TelemetryPusherConfig - SystemAPIUsers map[string]*internal_authz.SystemAPIUser + SystemAPIUsers map[string]*authz.SystemAPIUser } type InitProjections struct { @@ -68,12 +71,12 @@ func MustNewConfig(v *viper.Viper) *Config { err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( hooks.SliceTypeStringDecode[*domain.CustomMessageText], - hooks.SliceTypeStringDecode[internal_authz.RoleMapping], - hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser], + hooks.SliceTypeStringDecode[authz.RoleMapping], + hooks.MapTypeStringDecode[string, *authz.SystemAPIUser], hooks.MapHTTPHeaderStringDecode, - database.DecodeHook, + database.DecodeHook(false), actions.HTTPConfigDecodeHook, - hook.EnumHookFunc(internal_authz.MemberTypeString), + hook.EnumHookFunc(authz.MemberTypeString), hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), mapstructure.StringToTimeDurationHookFunc(), @@ -146,6 +149,7 @@ type Steps struct { s50IDPTemplate6UsePKCE *IDPTemplate6UsePKCE s51IDPTemplate6RootCA *IDPTemplate6RootCA s52IDPTemplate6LDAP2 *IDPTemplate6LDAP2 + s53InitPermittedOrgsFunction *InitPermittedOrgsFunction53 } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/river_queue_repeatable.go b/cmd/setup/river_queue_repeatable.go index 5248894a8f..bfbd3ee581 100644 --- a/cmd/setup/river_queue_repeatable.go +++ b/cmd/setup/river_queue_repeatable.go @@ -13,9 +13,6 @@ type RiverMigrateRepeatable struct { } func (mig *RiverMigrateRepeatable) Execute(ctx context.Context, _ eventstore.Event) error { - if mig.client.Type() != "postgres" { - return nil - } return queue.NewMigrator(mig.client).Execute(ctx) } diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 5d723ebeac..f4df9fc71b 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -60,7 +60,7 @@ func New() *cobra.Command { Short: "setup ZITADEL instance", Long: `sets up data to start ZITADEL. Requirements: -- cockroachdb`, +- postgreSQL`, Run: func(cmd *cobra.Command, args []string) { err := tls.ModeFromFlag(cmd) logging.OnError(err).Fatal("invalid tlsMode") @@ -139,7 +139,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) dbClient, err := database.Connect(config.Database, false) logging.OnError(err).Fatal("unable to connect to database") - config.Eventstore.Querier = old_es.NewCRDB(dbClient) + config.Eventstore.Querier = old_es.NewPostgres(dbClient) esV3 := new_es.NewEventstore(dbClient) config.Eventstore.Pusher = esV3 config.Eventstore.Searcher = esV3 @@ -169,7 +169,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s5LastFailed = &LastFailed{dbClient: dbClient.DB} steps.s6OwnerRemoveColumns = &OwnerRemoveColumns{dbClient: dbClient.DB} - steps.s7LogstoreTables = &LogstoreTables{dbClient: dbClient.DB, username: config.Database.Username(), dbType: config.Database.Type()} + steps.s7LogstoreTables = &LogstoreTables{dbClient: dbClient.DB, username: config.Database.Username()} steps.s8AuthTokens = &AuthTokenIndexes{dbClient: dbClient} steps.CorrectCreationDate.dbClient = dbClient steps.s12AddOTPColumns = &AddOTPColumns{dbClient: dbClient} @@ -211,6 +211,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s50IDPTemplate6UsePKCE = &IDPTemplate6UsePKCE{dbClient: dbClient} steps.s51IDPTemplate6RootCA = &IDPTemplate6RootCA{dbClient: dbClient} steps.s52IDPTemplate6LDAP2 = &IDPTemplate6LDAP2{dbClient: dbClient} + steps.s53InitPermittedOrgsFunction = &InitPermittedOrgsFunction53{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -252,6 +253,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s50IDPTemplate6UsePKCE, steps.s51IDPTemplate6RootCA, steps.s52IDPTemplate6LDAP2, + steps.s53InitPermittedOrgsFunction, } { setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") if setupErr != nil { @@ -350,8 +352,8 @@ func executeMigration(ctx context.Context, eventstoreClient *eventstore.Eventsto // under the folder/typ/filename path. // Typ describes the database dialect and may be omitted if no // dialect specific migration is specified. -func readStmt(fs embed.FS, folder, typ, filename string) (string, error) { - stmt, err := fs.ReadFile(path.Join(folder, typ, filename)) +func readStmt(fs embed.FS, folder, filename string) (string, error) { + stmt, err := fs.ReadFile(path.Join(folder, filename)) return string(stmt), err } @@ -364,16 +366,15 @@ type statement struct { // under the folder/type path. // Typ describes the database dialect and may be omitted if no // dialect specific migration is specified. -func readStatements(fs embed.FS, folder, typ string) ([]statement, error) { - basePath := path.Join(folder, typ) - dir, err := fs.ReadDir(basePath) +func readStatements(fs embed.FS, folder string) ([]statement, error) { + dir, err := fs.ReadDir(folder) if err != nil { return nil, err } statements := make([]statement, len(dir)) for i, file := range dir { statements[i].file = file.Name() - statements[i].query, err = readStmt(fs, folder, typ, file.Name()) + statements[i].query, err = readStmt(fs, folder, file.Name()) if err != nil { return nil, err } @@ -454,7 +455,7 @@ func startCommandsQueries( sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, 0, // not needed for projections @@ -479,7 +480,7 @@ func startCommandsQueries( authZRepo, err := authz.Start(queries, eventstoreClient, dbClient, keys.OIDC, config.ExternalSecure) logging.OnError(err).Fatal("unable to start authz repo") permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } commands, err := command.StartCommands(ctx, @@ -514,9 +515,6 @@ func startCommandsQueries( ) logging.OnError(err).Fatal("unable to start commands") - if !config.Notifications.LegacyEnabled && dbClient.Type() == "cockroach" { - logging.Fatal("notifications must be set to LegacyEnabled=true when using CockroachDB") - } q, err := queue.NewQueue(&queue.Config{ Client: dbClient, }) @@ -543,7 +541,6 @@ func startCommandsQueries( keys.SMS, keys.OIDC, config.OIDC.DefaultBackChannelLogoutLifetime, - dbClient, q, ) diff --git a/cmd/start/config.go b/cmd/start/config.go index 5e28e1dfb2..78b6f0afe0 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -11,7 +11,7 @@ import ( "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing" - internal_authz "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/api/saml" @@ -27,6 +27,7 @@ import ( "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/notification/handlers" @@ -56,6 +57,7 @@ type Config struct { Profiler profiler.Config Projections projection.Config Notifications handlers.WorkerConfig + Executions execution.WorkerConfig Auth auth_es.Config Admin admin_es.Config UserAgentCookie *middleware.UserAgentCookieConfig @@ -65,12 +67,13 @@ type Config struct { Login login.Config Console console.Config AssetStorage static_config.AssetStorageConfig - InternalAuthZ internal_authz.Config + InternalAuthZ authz.Config + SystemAuthZ authz.Config SystemDefaults systemdefaults.SystemDefaults EncryptionKeys *encryption.EncryptionKeyConfig DefaultInstance command.InstanceSetup AuditLogRetention time.Duration - SystemAPIUsers map[string]*internal_authz.SystemAPIUser + SystemAPIUsers map[string]*authz.SystemAPIUser CustomerPortal string Machine *id.Config Actions *actions.Config @@ -94,12 +97,12 @@ func MustNewConfig(v *viper.Viper) *Config { err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( hooks.SliceTypeStringDecode[*domain.CustomMessageText], - hooks.SliceTypeStringDecode[internal_authz.RoleMapping], - hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser], + hooks.SliceTypeStringDecode[authz.RoleMapping], + hooks.MapTypeStringDecode[string, *authz.SystemAPIUser], hooks.MapHTTPHeaderStringDecode, - database.DecodeHook, + database.DecodeHook(false), actions.HTTPConfigDecodeHook, - hook.EnumHookFunc(internal_authz.MemberTypeString), + hook.EnumHookFunc(authz.MemberTypeString), hooks.MapTypeStringDecode[domain.Feature, any], hooks.SliceTypeStringDecode[*command.SetQuota], hook.Base64ToBytesHookFunc(), diff --git a/cmd/start/start.go b/cmd/start/start.go index 1235708ef9..4c8709c4a7 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -35,6 +35,7 @@ import ( "github.com/zitadel/zitadel/internal/api" "github.com/zitadel/zitadel/internal/api/assets" internal_authz "github.com/zitadel/zitadel/internal/api/authz" + action_v2_beta "github.com/zitadel/zitadel/internal/api/grpc/action/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/admin" "github.com/zitadel/zitadel/internal/api/grpc/auth" feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2" @@ -45,11 +46,9 @@ import ( oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta" org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta" - action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/action/v3alpha" "github.com/zitadel/zitadel/internal/api/grpc/resources/debug_events/debug_events" user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha" userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha" - "github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3" saml_v2 "github.com/zitadel/zitadel/internal/api/grpc/saml/v2" session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta" @@ -58,6 +57,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/system" user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" + webkey "github.com/zitadel/zitadel/internal/api/grpc/webkey/v2beta" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/idp" @@ -82,13 +82,14 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql" new_es "github.com/zitadel/zitadel/internal/eventstore/v3" + "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/integration/sink" "github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/logstore/emitters/access" - "github.com/zitadel/zitadel/internal/logstore/emitters/execution" - "github.com/zitadel/zitadel/internal/logstore/emitters/stdout" + emit_execution "github.com/zitadel/zitadel/internal/logstore/emitters/execution" + emit_stdout "github.com/zitadel/zitadel/internal/logstore/emitters/stdout" "github.com/zitadel/zitadel/internal/logstore/record" "github.com/zitadel/zitadel/internal/net" "github.com/zitadel/zitadel/internal/notification" @@ -107,7 +108,7 @@ func New(server chan<- *Server) *cobra.Command { Short: "starts ZITADEL instance", Long: `starts ZITADEL. Requirements: -- cockroachdb`, +- postgreSQL`, RunE: func(cmd *cobra.Command, args []string) error { err := cmd_tls.ModeFromFlag(cmd) if err != nil { @@ -163,7 +164,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server config.Eventstore.Pusher = new_es.NewEventstore(dbClient) config.Eventstore.Searcher = new_es.NewEventstore(dbClient) - config.Eventstore.Querier = old_es.NewCRDB(dbClient) + config.Eventstore.Querier = old_es.NewPostgres(dbClient) eventstoreClient := eventstore.NewEventstore(config.Eventstore) eventstoreV4 := es_v4.NewEventstoreFromOne(es_v4_pg.New(dbClient, &es_v4_pg.Config{ MaxRetries: config.Eventstore.MaxRetries, @@ -193,7 +194,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, config.AuditLogRetention, @@ -209,7 +210,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server return fmt.Errorf("error starting authz repo: %w", err) } permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } storage, err := config.AssetStorage.NewStorage(dbClient.DB) @@ -257,11 +258,12 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server defer closeSink() clock := clockpkg.New() - actionsExecutionStdoutEmitter, err := logstore.NewEmitter[*record.ExecutionLog](ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Execution.Stdout.Enabled}, stdout.NewStdoutEmitter[*record.ExecutionLog]()) + actionsExecutionStdoutEmitter, err := logstore.NewEmitter(ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Execution.Stdout.Enabled}, emit_stdout.NewStdoutEmitter[*record.ExecutionLog]()) if err != nil { return err } - actionsExecutionDBEmitter, err := logstore.NewEmitter[*record.ExecutionLog](ctx, clock, config.Quotas.Execution, execution.NewDatabaseLogStorage(dbClient, commands, queries)) + + actionsExecutionDBEmitter, err := logstore.NewEmitter(ctx, clock, config.Quotas.Execution, emit_execution.NewDatabaseLogStorage(dbClient, commands, queries)) if err != nil { return err } @@ -300,11 +302,20 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server keys.SMS, keys.OIDC, config.OIDC.DefaultBackChannelLogoutLifetime, - dbClient, q, ) notification.Start(ctx) + execution.Register( + ctx, + config.Projections.Customizations["executions"], + config.Executions, + queries, + eventstoreClient.EventTypes(), + q, + ) + execution.Start(ctx) + if err = q.Start(ctx); err != nil { return err } @@ -395,23 +406,23 @@ func startAPIs( return nil, err } - accessStdoutEmitter, err := logstore.NewEmitter[*record.AccessLog](ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Access.Stdout.Enabled}, stdout.NewStdoutEmitter[*record.AccessLog]()) + accessStdoutEmitter, err := logstore.NewEmitter(ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Access.Stdout.Enabled}, emit_stdout.NewStdoutEmitter[*record.AccessLog]()) if err != nil { return nil, err } - accessDBEmitter, err := logstore.NewEmitter[*record.AccessLog](ctx, clock, &config.Quotas.Access.EmitterConfig, access.NewDatabaseLogStorage(dbClient, commands, queries)) + accessDBEmitter, err := logstore.NewEmitter(ctx, clock, &config.Quotas.Access.EmitterConfig, access.NewDatabaseLogStorage(dbClient, commands, queries)) if err != nil { return nil, err } - accessSvc := logstore.New[*record.AccessLog](queries, accessDBEmitter, accessStdoutEmitter) + accessSvc := logstore.New(queries, accessDBEmitter, accessStdoutEmitter) exhaustedCookieHandler := http_util.NewCookieHandler( http_util.WithUnsecure(), http_util.WithNonHttpOnly(), http_util.WithMaxAge(int(math.Floor(config.Quotas.Access.ExhaustedCookieMaxAge.Seconds()))), ) limitingAccessInterceptor := middleware.NewAccessInterceptor(accessSvc, exhaustedCookieHandler, &config.Quotas.Access.AccessConfig) - apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.ExternalDomain, append(config.InstanceHostHeaders, config.PublicHostHeaders...), limitingAccessInterceptor) + apis, err := api.New(ctx, config.Port, router, queries, verifier, config.SystemAuthZ, config.InternalAuthZ, tlsConfig, config.ExternalDomain, append(config.InstanceHostHeaders, config.PublicHostHeaders...), limitingAccessInterceptor) if err != nil { return nil, fmt.Errorf("error creating api %w", err) } @@ -477,7 +488,7 @@ func startAPIs( if err := apis.RegisterService(ctx, idp_v2.CreateServer(commands, queries, permissionCheck)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(config.SystemDefaults, commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { + if err := apis.RegisterService(ctx, action_v2_beta.CreateServer(config.SystemDefaults, commands, queries, domain.AllActionFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { return nil, err } if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil { @@ -494,7 +505,7 @@ func startAPIs( } instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...) assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) - apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) + apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.SystemAuthZ, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler)) @@ -538,7 +549,7 @@ func startAPIs( keys.User, &config.SCIM, instanceInterceptor.HandlerFuncWithError, - middleware.AuthorizationInterceptor(verifier, config.InternalAuthZ).HandlerFuncWithError)) + middleware.AuthorizationInterceptor(verifier, config.SystemAuthZ, config.InternalAuthZ).HandlerFuncWithError)) c, err := console.Start(config.Console, config.ExternalSecure, oidcServer.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, limitingAccessInterceptor, config.CustomerPortal) if err != nil { @@ -604,7 +615,7 @@ func listen(ctx context.Context, router *mux.Router, port uint16, tlsConfig *tls go func() { logging.Infof("server is listening on %s", lis.Addr().String()) if tlsConfig != nil { - //we don't need to pass the files here, because we already initialized the TLS config on the server + // we don't need to pass the files here, because we already initialized the TLS config on the server errCh <- http1Server.ServeTLS(lis, "", "") } else { errCh <- http1Server.Serve(lis) diff --git a/cmd/start/start_from_init.go b/cmd/start/start_from_init.go index 38a6a6c4d1..62d705b33c 100644 --- a/cmd/start/start_from_init.go +++ b/cmd/start/start_from_init.go @@ -21,7 +21,7 @@ Second the initial events are created. Last ZITADEL starts. Requirements: -- cockroachdb`, +- postgreSQL`, Run: func(cmd *cobra.Command, args []string) { err := tls.ModeFromFlag(cmd) logging.OnError(err).Fatal("invalid tlsMode") diff --git a/console/package.json b/console/package.json index 8e1df9578e..77a8a40147 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.4", + "@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", diff --git a/console/src/app/app-routing.module.ts b/console/src/app/app-routing.module.ts index 4900c8e424..582f65d8af 100644 --- a/console/src/app/app-routing.module.ts +++ b/console/src/app/app-routing.module.ts @@ -1,8 +1,8 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { AuthGuard } from './guards/auth.guard'; -import { RoleGuard } from './guards/role.guard'; +import { authGuard } from './guards/auth.guard'; +import { roleGuard } from './guards/role-guard'; import { UserGrantContext } from './modules/user-grants/user-grants-datasource'; import { OrgCreateComponent } from './pages/org-create/org-create.component'; @@ -10,7 +10,7 @@ const routes: Routes = [ { path: '', loadChildren: () => import('./pages/home/home.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['.'], }, @@ -22,7 +22,7 @@ const routes: Routes = [ { path: 'orgs/create', component: OrgCreateComponent, - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['(org.create)?(iam.write)?'], }, @@ -31,12 +31,12 @@ const routes: Routes = [ { path: 'orgs', loadChildren: () => import('./pages/org-list/org-list.module'), - canActivate: [AuthGuard], + canActivate: [authGuard], }, { path: 'granted-projects', loadChildren: () => import('./pages/projects/granted-projects/granted-projects.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['project.grant.read'], }, @@ -44,20 +44,20 @@ const routes: Routes = [ { path: 'projects', loadChildren: () => import('./pages/projects/projects.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['project.read'], }, }, { path: 'users', - canActivate: [AuthGuard], + canActivate: [authGuard], loadChildren: () => import('src/app/pages/users/users.module'), }, { path: 'instance', loadChildren: () => import('./pages/instance/instance.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['iam.read', 'iam.write'], }, @@ -65,7 +65,7 @@ const routes: Routes = [ { path: 'org', loadChildren: () => import('./pages/orgs/org.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['org.read'], }, @@ -73,7 +73,7 @@ const routes: Routes = [ { path: 'actions', loadChildren: () => import('./pages/actions/actions.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['org.action.read', 'org.flow.read'], }, @@ -81,7 +81,7 @@ const routes: Routes = [ { path: 'grants', loadChildren: () => import('./pages/grants/grants.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { context: UserGrantContext.NONE, roles: ['user.grant.read'], @@ -89,12 +89,12 @@ const routes: Routes = [ }, { path: 'grant-create', - canActivate: [AuthGuard], + canActivate: [authGuard], children: [ { path: 'project/:projectid/grant/:grantid', loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'), - canActivate: [RoleGuard], + canActivate: [roleGuard], data: { roles: ['user.grant.write'], }, @@ -102,7 +102,7 @@ const routes: Routes = [ { path: 'project/:projectid', loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'), - canActivate: [RoleGuard], + canActivate: [roleGuard], data: { roles: ['user.grant.write'], }, @@ -110,7 +110,7 @@ const routes: Routes = [ { path: 'user/:userid', loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'), - canActivate: [RoleGuard], + canActivate: [roleGuard], data: { roles: ['user.grant.write'], }, @@ -118,7 +118,7 @@ const routes: Routes = [ { path: '', loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'), - canActivate: [RoleGuard], + canActivate: [roleGuard], data: { roles: ['user.grant.write'], }, @@ -128,7 +128,7 @@ const routes: Routes = [ { path: 'org-settings', loadChildren: () => import('./pages/org-settings/org-settings.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['policy.read'], }, diff --git a/console/src/app/app.module.ts b/console/src/app/app.module.ts index 96ae614c9e..d6e7e60bea 100644 --- a/console/src/app/app.module.ts +++ b/console/src/app/app.module.ts @@ -18,6 +18,7 @@ import localeNl from '@angular/common/locales/nl'; import localeSv from '@angular/common/locales/sv'; import localeHu from '@angular/common/locales/hu'; import localeKo from '@angular/common/locales/ko'; +import localeRo from '@angular/common/locales/ro'; import { APP_INITIALIZER, NgModule } from '@angular/core'; import { MatNativeDateModule } from '@angular/material/core'; import { MatDialogModule } from '@angular/material/dialog'; @@ -32,9 +33,6 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; import * as i18nIsoCountries from 'i18n-iso-countries'; import { from, Observable } from 'rxjs'; -import { AuthGuard } from 'src/app/guards/auth.guard'; -import { RoleGuard } from 'src/app/guards/role.guard'; -import { UserGuard } from 'src/app/guards/user.guard'; import { InfoOverlayModule } from 'src/app/modules/info-overlay/info-overlay.module'; import { AssetService } from 'src/app/services/asset.service'; import { AppRoutingModule } from './app-routing.module'; @@ -112,6 +110,8 @@ registerLocaleData(localeHu); i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/hu.json')); registerLocaleData(localeKo); i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/ko.json')); +registerLocaleData(localeRo); +i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/ro.json')); export class WebpackTranslateLoader implements TranslateLoader { getTranslation(lang: string): Observable { @@ -170,9 +170,6 @@ const authConfig: AuthConfig = { ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }), ], providers: [ - AuthGuard, - RoleGuard, - UserGuard, ThemeService, EnvironmentService, ExhaustedService, diff --git a/console/src/app/components/feature-toggle/feature-toggle.component.html b/console/src/app/components/feature-toggle/feature-toggle.component.html new file mode 100644 index 0000000000..cb97f1b746 --- /dev/null +++ b/console/src/app/components/feature-toggle/feature-toggle.component.html @@ -0,0 +1,40 @@ +
+ {{ 'SETTING.FEATURES.' + (toggleStateKey | uppercase) | translate }} +
+ + +
+ {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} + +
+
+
+ +
+ {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} +
+
+
+
+
+ + {{ i18nDescription }} +
diff --git a/console/src/app/components/feature-toggle/feature-toggle.component.scss b/console/src/app/components/feature-toggle/feature-toggle.component.scss new file mode 100644 index 0000000000..b1e7c1a3f1 --- /dev/null +++ b/console/src/app/components/feature-toggle/feature-toggle.component.scss @@ -0,0 +1,39 @@ +.feature-row { + display: flex; + flex-direction: column; + padding-bottom: 1rem; + + .row { + display: flex; + align-items: center; + justify-content: space-between; + + .buttongroup { + margin-right: 0.5rem; + margin-top: 0.5rem; + + .toggle-row { + display: flex; + align-items: center; + + i { + margin-right: 0.5rem; + } + + .info-i { + font-size: 1.2rem; + margin-left: 0.5rem; + margin-right: 0; + } + + .current-dot { + height: 8px; + width: 8px; + border-radius: 50%; + margin-left: 0.5rem; + background-color: rgb(59, 128, 247); + } + } + } + } +} diff --git a/console/src/app/components/feature-toggle/feature-toggle.component.ts b/console/src/app/components/feature-toggle/feature-toggle.component.ts new file mode 100644 index 0000000000..fab0b31d48 --- /dev/null +++ b/console/src/app/components/feature-toggle/feature-toggle.component.ts @@ -0,0 +1,45 @@ +import { AsyncPipe, NgIf, UpperCasePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module'; +import { ToggleStateKeys, ToggleStates } from '../features/features.component'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { FormsModule } from '@angular/forms'; +import { Source } from '@zitadel/proto/zitadel/feature/v2/feature_pb'; +import { ReplaySubject } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Component({ + standalone: true, + selector: 'cnsl-feature-toggle', + templateUrl: './feature-toggle.component.html', + styleUrls: ['./feature-toggle.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + MatButtonToggleModule, + UpperCasePipe, + TranslateModule, + FormsModule, + MatTooltipModule, + InfoSectionModule, + AsyncPipe, + NgIf, + ], +}) +export class FeatureToggleComponent { + @Input({ required: true }) toggleStateKey!: TKey; + @Input({ required: true }) + set toggleState(toggleState: TValue) { + // we copy the toggleState so we can mutate it + this.toggleState$.next(structuredClone(toggleState)); + } + + @Output() readonly toggleChange = new EventEmitter(); + + protected readonly Source = Source; + protected readonly toggleState$ = new ReplaySubject(1); + protected readonly isInherited$ = this.toggleState$.pipe( + map(({ source }) => source == Source.SYSTEM || source == Source.UNSPECIFIED), + ); +} diff --git a/console/src/app/components/feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component.html b/console/src/app/components/feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component.html new file mode 100644 index 0000000000..c1feeb894c --- /dev/null +++ b/console/src/app/components/feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component.html @@ -0,0 +1,27 @@ + + + {{ 'SETTING.FEATURES.LOGINV2_BASEURI' | translate }} + + + + diff --git a/console/src/app/components/feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component.ts b/console/src/app/components/feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component.ts new file mode 100644 index 0000000000..01648d22ad --- /dev/null +++ b/console/src/app/components/feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component.ts @@ -0,0 +1,47 @@ +import { ChangeDetectionStrategy, Component, DestroyRef, EventEmitter, Input, Output } from '@angular/core'; +import { FeatureToggleComponent } from '../feature-toggle.component'; +import { ToggleStates } from 'src/app/components/features/features.component'; +import { distinctUntilKeyChanged, ReplaySubject } from 'rxjs'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { AsyncPipe, NgIf } from '@angular/common'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { InputModule } from 'src/app/modules/input/input.module'; +import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; +import { MatButtonModule } from '@angular/material/button'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatTooltipModule } from '@angular/material/tooltip'; + +@Component({ + standalone: true, + selector: 'cnsl-login-v2-feature-toggle', + templateUrl: './login-v2-feature-toggle.component.html', + imports: [ + FeatureToggleComponent, + AsyncPipe, + NgIf, + ReactiveFormsModule, + InputModule, + HasRolePipeModule, + MatButtonModule, + TranslateModule, + MatTooltipModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoginV2FeatureToggleComponent { + @Input({ required: true }) + set toggleState(toggleState: ToggleStates['loginV2']) { + this.toggleState$.next(toggleState); + } + @Output() + public toggleChanged = new EventEmitter(); + + protected readonly toggleState$ = new ReplaySubject(1); + protected readonly baseUri = new FormControl('', { nonNullable: true, validators: [Validators.required] }); + + constructor(destroyRef: DestroyRef) { + this.toggleState$.pipe(distinctUntilKeyChanged('baseUri'), takeUntilDestroyed(destroyRef)).subscribe(({ baseUri }) => { + this.baseUri.setValue(baseUri); + }); + } +} diff --git a/console/src/app/components/features/features.component.html b/console/src/app/components/features/features.component.html index e663569210..30d8f629af 100644 --- a/console/src/app/components/features/features.component.html +++ b/console/src/app/components/features/features.component.html @@ -13,402 +13,20 @@

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

- - +
-
- {{ 'SETTING.FEATURES.LOGINDEFAULTORG' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- - {{ - 'SETTING.FEATURES.LOGINDEFAULTORG_DESCRIPTION' | translate - }} -
- -
- {{ 'SETTING.FEATURES.OIDCLEGACYINTROSPECTION' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- {{ - 'SETTING.FEATURES.OIDCLEGACYINTROSPECTION_DESCRIPTION' | translate - }} -
- -
- {{ 'SETTING.FEATURES.OIDCTOKENEXCHANGE' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- {{ - 'SETTING.FEATURES.OIDCTOKENEXCHANGE_DESCRIPTION' | translate - }} -
- -
- {{ 'SETTING.FEATURES.OIDCTRIGGERINTROSPECTIONPROJECTIONS' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- {{ - 'SETTING.FEATURES.OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION' | translate - }} -
- -
- {{ 'SETTING.FEATURES.USERSCHEMA' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- {{ - 'SETTING.FEATURES.USERSCHEMA_DESCRIPTION' | translate - }} -
- -
- {{ 'SETTING.FEATURES.ACTIONS' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- {{ 'SETTING.FEATURES.ACTIONS_DESCRIPTION' | translate }} -
- -
- {{ 'SETTING.FEATURES.OIDCSINGLEV1SESSIONTERMINATION' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- {{ - 'SETTING.FEATURES.OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION' | translate - }} -
+ +
- - - - {{ 'SETTING.FEATURES.SOURCE.' + source | translate }} - - diff --git a/console/src/app/components/features/features.component.ts b/console/src/app/components/features/features.component.ts index 899670c95e..d95bbdde43 100644 --- a/console/src/app/components/features/features.component.ts +++ b/console/src/app/components/features/features.component.ts @@ -11,32 +11,46 @@ import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { CardModule } from 'src/app/modules/card/card.module'; import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; -import { Source } from 'src/app/proto/generated/zitadel/feature/v2beta/feature_pb'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; -import { FeatureService } from 'src/app/services/feature.service'; import { ToastService } from 'src/app/services/toast.service'; +import { FeatureToggleComponent } from '../feature-toggle/feature-toggle.component'; +import { NewFeatureService } from 'src/app/services/new-feature.service'; import { GetInstanceFeaturesResponse, - SetInstanceFeaturesRequest, -} from 'src/app/proto/generated/zitadel/feature/v2/instance_pb'; + SetInstanceFeaturesRequestSchema, +} from '@zitadel/proto/zitadel/feature/v2/instance_pb'; +import { Source } from '@zitadel/proto/zitadel/feature/v2/feature_pb'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { firstValueFrom, Observable, ReplaySubject, shareReplay, switchMap } from 'rxjs'; +import { filter, map, startWith } from 'rxjs/operators'; +import { LoginV2FeatureToggleComponent } from '../feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component'; -enum ToggleState { - ENABLED = 'ENABLED', - DISABLED = 'DISABLED', - INHERITED = 'INHERITED', -} +// to add a new feature, add the key here and in the FEATURE_KEYS array +const FEATURE_KEYS = [ + 'consoleUseV2UserApi', + 'debugOidcParentError', + 'disableUserTokenEvent', + 'enableBackChannelLogout', + // 'improvedPerformance', + 'loginDefaultOrg', + 'oidcLegacyIntrospection', + 'oidcSingleV1SessionTermination', + 'oidcTokenExchange', + 'oidcTriggerIntrospectionProjections', + 'permissionCheckV2', + 'userSchema', + 'webKey', +] as const; -type FeatureState = { source: Source; state: ToggleState }; -type ToggleStates = { - loginDefaultOrg?: FeatureState; - oidcTriggerIntrospectionProjections?: FeatureState; - oidcLegacyIntrospection?: FeatureState; - userSchema?: FeatureState; - oidcTokenExchange?: FeatureState; - actions?: FeatureState; - oidcSingleV1SessionTermination?: FeatureState; +export type ToggleState = { source: Source; enabled: boolean }; +export type ToggleStates = { + [key in (typeof FEATURE_KEYS)[number]]: ToggleState; +} & { + loginV2: ToggleState & { baseUri: string }; }; +export type ToggleStateKeys = keyof ToggleStates; + @Component({ imports: [ CommonModule, @@ -51,6 +65,8 @@ type ToggleStates = { InfoSectionModule, MatTooltipModule, HasRoleModule, + FeatureToggleComponent, + LoginV2FeatureToggleComponent, ], standalone: true, selector: 'cnsl-features', @@ -58,16 +74,15 @@ type ToggleStates = { styleUrls: ['./features.component.scss'], }) export class FeaturesComponent { - protected featureData: GetInstanceFeaturesResponse.AsObject | undefined; - - protected toggleStates: ToggleStates | undefined; - protected Source: any = Source; - protected ToggleState: any = ToggleState; + private readonly refresh$ = new ReplaySubject(1); + protected readonly toggleStates$: Observable; + protected readonly Source = Source; + protected readonly FEATURE_KEYS = FEATURE_KEYS; constructor( - private featureService: FeatureService, - private breadcrumbService: BreadcrumbService, - private toast: ToastService, + private readonly featureService: NewFeatureService, + private readonly breadcrumbService: BreadcrumbService, + private readonly toast: ToastService, ) { const breadcrumbs = [ new Breadcrumb({ @@ -78,153 +93,84 @@ export class FeaturesComponent { ]; this.breadcrumbService.setBreadcrumb(breadcrumbs); - this.getFeatures(true); + this.toggleStates$ = this.getToggleStates().pipe(shareReplay({ refCount: true, bufferSize: 1 })); } - public validateAndSave() { - this.featureService.resetInstanceFeatures().then(() => { - const req = new SetInstanceFeaturesRequest(); - let changed = false; - - console.log(this.toggleStates); - - if (this.toggleStates?.loginDefaultOrg?.state !== ToggleState.INHERITED) { - req.setLoginDefaultOrg(this.toggleStates?.loginDefaultOrg?.state === ToggleState.ENABLED); - changed = true; - } - if (this.toggleStates?.oidcTriggerIntrospectionProjections?.state !== ToggleState.INHERITED) { - req.setOidcTriggerIntrospectionProjections( - this.toggleStates?.oidcTriggerIntrospectionProjections?.state === ToggleState.ENABLED, - ); - changed = true; - } - if (this.toggleStates?.oidcLegacyIntrospection?.state !== ToggleState.INHERITED) { - req.setOidcLegacyIntrospection(this.toggleStates?.oidcLegacyIntrospection?.state === ToggleState.ENABLED); - changed = true; - } - if (this.toggleStates?.userSchema?.state !== ToggleState.INHERITED) { - req.setUserSchema(this.toggleStates?.userSchema?.state === ToggleState.ENABLED); - changed = true; - } - if (this.toggleStates?.oidcTokenExchange?.state !== ToggleState.INHERITED) { - req.setOidcTokenExchange(this.toggleStates?.oidcTokenExchange?.state === ToggleState.ENABLED); - changed = true; - } - if (this.toggleStates?.actions?.state !== ToggleState.INHERITED) { - req.setActions(this.toggleStates?.actions?.state === ToggleState.ENABLED); - changed = true; - } - if (this.toggleStates?.oidcSingleV1SessionTermination?.state !== ToggleState.INHERITED) { - req.setOidcSingleV1SessionTermination( - this.toggleStates?.oidcSingleV1SessionTermination?.state === ToggleState.ENABLED, - ); - changed = true; - } - - if (changed) { - this.featureService - .setInstanceFeatures(req) - .then(() => { - this.toast.showInfo('POLICY.TOAST.SET', true); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - }); + private getToggleStates() { + return this.refresh$.pipe( + startWith(true), + switchMap(async () => { + try { + return await this.featureService.getInstanceFeatures(); + } catch (error) { + this.toast.showError(error); + return undefined; + } + }), + filter(Boolean), + map((res) => this.createToggleStates(res)), + ); } - private getFeatures(inheritance: boolean) { - this.featureService.getInstanceFeatures(inheritance).then((instanceFeaturesResponse) => { - this.featureData = instanceFeaturesResponse.toObject(); - console.log(this.featureData); - - this.toggleStates = { - loginDefaultOrg: { - source: this.featureData.loginDefaultOrg?.source || Source.SOURCE_SYSTEM, - state: - this.featureData.loginDefaultOrg?.source === Source.SOURCE_SYSTEM || - this.featureData.loginDefaultOrg?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.loginDefaultOrg?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, + private createToggleStates(featureData: GetInstanceFeaturesResponse): ToggleStates { + return FEATURE_KEYS.reduce( + (acc, key) => { + const feature = featureData[key]; + acc[key] = { + source: feature?.source ?? Source.SYSTEM, + enabled: !!feature?.enabled, + }; + return acc; + }, + { + // to add special feature flags they have to be mapped here + loginV2: { + source: featureData.loginV2?.source ?? Source.SYSTEM, + enabled: !!featureData.loginV2?.required, + baseUri: featureData.loginV2?.baseUri ?? '', }, - oidcTriggerIntrospectionProjections: { - source: this.featureData.oidcTriggerIntrospectionProjections?.source || Source.SOURCE_SYSTEM, - state: - this.featureData.oidcTriggerIntrospectionProjections?.source === Source.SOURCE_SYSTEM || - this.featureData.oidcTriggerIntrospectionProjections?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.oidcTriggerIntrospectionProjections?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - oidcLegacyIntrospection: { - source: this.featureData.oidcLegacyIntrospection?.source || Source.SOURCE_SYSTEM, - state: - this.featureData.oidcLegacyIntrospection?.source === Source.SOURCE_SYSTEM || - this.featureData.oidcLegacyIntrospection?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.oidcLegacyIntrospection?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - userSchema: { - source: this.featureData.userSchema?.source || Source.SOURCE_SYSTEM, - state: - this.featureData.userSchema?.source === Source.SOURCE_SYSTEM || - this.featureData.userSchema?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.userSchema?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - oidcTokenExchange: { - source: this.featureData.oidcTokenExchange?.source || Source.SOURCE_SYSTEM, - state: - this.featureData.oidcTokenExchange?.source === Source.SOURCE_SYSTEM || - this.featureData.oidcTokenExchange?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.oidcTokenExchange?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - actions: { - source: Source.SOURCE_SYSTEM, - state: - this.featureData.actions?.source === Source.SOURCE_SYSTEM || - this.featureData.actions?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.actions?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - oidcSingleV1SessionTermination: { - source: this.featureData.oidcSingleV1SessionTermination?.source || Source.SOURCE_SYSTEM, - state: - this.featureData.oidcSingleV1SessionTermination?.source === Source.SOURCE_SYSTEM || - this.featureData.oidcSingleV1SessionTermination?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.oidcSingleV1SessionTermination?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - }; - }); + } as ToggleStates, + ); } - public resetSettings(): void { - this.featureService - .resetInstanceFeatures() - .then(() => { - this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true); - setTimeout(() => { - this.getFeatures(true); - }, 1000); - }) - .catch((error) => { - this.toast.showError(error); - }); + public async saveFeatures(key: TKey, value: TValue) { + const toggleStates = { ...(await firstValueFrom(this.toggleStates$)), [key]: value }; + + const req = FEATURE_KEYS.reduce>((acc, key) => { + acc[key] = toggleStates[key].enabled; + return acc; + }, {}); + + // to save special flags they have to be handled here + req.loginV2 = { + required: toggleStates.loginV2.enabled, + baseUri: toggleStates.loginV2.baseUri, + }; + + try { + await this.featureService.setInstanceFeatures(req); + + // needed because of eventual consistency + await new Promise((res) => setTimeout(res, 1000)); + this.refresh$.next(true); + + this.toast.showInfo('POLICY.TOAST.SET', true); + } catch (error) { + this.toast.showError(error); + } + } + + public async resetFeatures() { + try { + await this.featureService.resetInstanceFeatures(); + + // needed because of eventual consistency + await new Promise((res) => setTimeout(res, 1000)); + this.refresh$.next(true); + + this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true); + } catch (error) { + this.toast.showError(error); + } } } diff --git a/console/src/app/directives/type-safe-cell-def/type-safe-cell-def.directive.ts b/console/src/app/directives/type-safe-cell-def/type-safe-cell-def.directive.ts new file mode 100644 index 0000000000..a3a145964b --- /dev/null +++ b/console/src/app/directives/type-safe-cell-def/type-safe-cell-def.directive.ts @@ -0,0 +1,16 @@ +import { Directive, Input } from '@angular/core'; +import { DataSource } from '@angular/cdk/collections'; +import { MatCellDef } from '@angular/material/table'; +import { CdkCellDef } from '@angular/cdk/table'; + +@Directive({ + selector: '[cnslCellDef]', + providers: [{ provide: CdkCellDef, useExisting: TypeSafeCellDefDirective }], +}) +export class TypeSafeCellDefDirective extends MatCellDef { + @Input({ required: true }) cnslCellDefDataSource!: DataSource; + + static ngTemplateContextGuard(_dir: TypeSafeCellDefDirective, _ctx: any): _ctx is { $implicit: T; index: number } { + return true; + } +} diff --git a/console/src/app/directives/type-safe-cell-def/type-safe-cell-def.module.ts b/console/src/app/directives/type-safe-cell-def/type-safe-cell-def.module.ts new file mode 100644 index 0000000000..01557ed65c --- /dev/null +++ b/console/src/app/directives/type-safe-cell-def/type-safe-cell-def.module.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { TypeSafeCellDefDirective } from './type-safe-cell-def.directive'; + +@NgModule({ + declarations: [TypeSafeCellDefDirective], + imports: [CommonModule], + exports: [TypeSafeCellDefDirective], +}) +export class TypeSafeCellDefModule {} diff --git a/console/src/app/guards/auth.guard.ts b/console/src/app/guards/auth.guard.ts index ca996c3312..8f1ebaabde 100644 --- a/console/src/app/guards/auth.guard.ts +++ b/console/src/app/guards/auth.guard.ts @@ -1,34 +1,26 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { inject } from '@angular/core'; +import { CanActivateFn } from '@angular/router'; import { AuthConfig } from 'angular-oauth2-oidc'; -import { Observable } from 'rxjs'; import { AuthenticationService } from '../services/authentication.service'; -@Injectable({ - providedIn: 'root', -}) -export class AuthGuard { - constructor(private auth: AuthenticationService) {} +export const authGuard: CanActivateFn = (route) => { + const auth = inject(AuthenticationService); - public canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, - ): Observable | Promise | Promise | boolean { - if (!this.auth.authenticated) { - if (route.queryParams && route.queryParams['login_hint']) { - const hint = route.queryParams['login_hint']; - const configWithPrompt: Partial = { - customQueryParams: { - login_hint: hint, - }, - }; - console.log(`authenticate with login_hint: ${hint}`); - this.auth.authenticate(configWithPrompt); - } else { - return this.auth.authenticate(); - } + if (!auth.authenticated) { + if (route.queryParams && route.queryParams['login_hint']) { + const hint = route.queryParams['login_hint']; + const configWithPrompt: Partial = { + customQueryParams: { + login_hint: hint, + }, + }; + console.log(`authenticate with login_hint: ${hint}`); + auth.authenticate(configWithPrompt).then(); + } else { + return auth.authenticate(); } - return this.auth.authenticated; } -} + + return auth.authenticated; +}; diff --git a/console/src/app/guards/role-guard.ts b/console/src/app/guards/role-guard.ts new file mode 100644 index 0000000000..887b9e8802 --- /dev/null +++ b/console/src/app/guards/role-guard.ts @@ -0,0 +1,9 @@ +import { inject } from '@angular/core'; +import { CanActivateFn } from '@angular/router'; + +import { GrpcAuthService } from '../services/grpc-auth.service'; + +export const roleGuard: CanActivateFn = (route) => { + const authService = inject(GrpcAuthService); + return authService.isAllowed(route.data['roles'], route.data['requiresAll']); +}; diff --git a/console/src/app/guards/role.guard.ts b/console/src/app/guards/role.guard.ts deleted file mode 100644 index 951ffa1b60..0000000000 --- a/console/src/app/guards/role.guard.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -import { Observable } from 'rxjs'; - -import { GrpcAuthService } from '../services/grpc-auth.service'; - -@Injectable({ - providedIn: 'root', -}) -export class RoleGuard { - constructor(private authService: GrpcAuthService) {} - - public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.authService.isAllowed(route.data['roles'], route.data['requiresAll']); - } -} diff --git a/console/src/app/guards/user-guard.ts b/console/src/app/guards/user-guard.ts new file mode 100644 index 0000000000..fc97cf5a2e --- /dev/null +++ b/console/src/app/guards/user-guard.ts @@ -0,0 +1,21 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { map, take } from 'rxjs'; + +import { GrpcAuthService } from '../services/grpc-auth.service'; + +export const userGuard: CanActivateFn = (route) => { + const authService = inject(GrpcAuthService); + const router = inject(Router); + + return authService.user.pipe( + take(1), + map((user) => { + const isMe = user?.id === route.params['id']; + if (isMe) { + router.navigate(['/users', 'me']).then(); + } + return !isMe; + }), + ); +}; diff --git a/console/src/app/guards/user.guard.ts b/console/src/app/guards/user.guard.ts deleted file mode 100644 index b4527753fe..0000000000 --- a/console/src/app/guards/user.guard.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { map, Observable, take } from 'rxjs'; - -import { GrpcAuthService } from '../services/grpc-auth.service'; - -@Injectable({ - providedIn: 'root', -}) -export class UserGuard { - constructor( - private authService: GrpcAuthService, - private router: Router, - ) {} - - public canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, - ): Observable | Promise | boolean { - return this.authService.user.pipe( - take(1), - map((user) => { - const isMe = user?.id === route.params['id']; - if (isMe) { - this.router.navigate(['/users', 'me']); - } - return !isMe; - }), - ); - } -} diff --git a/console/src/app/modules/accounts-card/accounts-card.component.html b/console/src/app/modules/accounts-card/accounts-card.component.html index 1ed1649545..24fe098331 100644 --- a/console/src/app/modules/accounts-card/accounts-card.component.html +++ b/console/src/app/modules/accounts-card/accounts-card.component.html @@ -1,7 +1,6 @@ -
+
{{ 'USER.EDITACCOUNT' | translate }} diff --git a/console/src/app/modules/accounts-card/accounts-card.component.ts b/console/src/app/modules/accounts-card/accounts-card.component.ts index 2676a5bcf5..273af86467 100644 --- a/console/src/app/modules/accounts-card/accounts-card.component.ts +++ b/console/src/app/modules/accounts-card/accounts-card.component.ts @@ -1,64 +1,158 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, NgIterable, Output } from '@angular/core'; import { Router } from '@angular/router'; import { AuthConfig } from 'angular-oauth2-oidc'; -import { Session, User, UserState } from 'src/app/proto/generated/zitadel/user_pb'; +import { SessionState as V1SessionState, User, UserState } from 'src/app/proto/generated/zitadel/user_pb'; import { AuthenticationService } from 'src/app/services/authentication.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { toSignal } from '@angular/core/rxjs-interop'; +import { SessionService } from 'src/app/services/session.service'; +import { + catchError, + defer, + from, + map, + mergeMap, + Observable, + of, + ReplaySubject, + shareReplay, + switchMap, + timeout, + TimeoutError, + toArray, +} from 'rxjs'; +import { NewFeatureService } from 'src/app/services/new-feature.service'; +import { ToastService } from 'src/app/services/toast.service'; +import { SessionState as V2SessionState } from '@zitadel/proto/zitadel/user_pb'; +import { filter, withLatestFrom } from 'rxjs/operators'; + +interface V1AndV2Session { + displayName: string; + avatarUrl: string; + loginName: string; + userName: string; + authState: V1SessionState | V2SessionState; +} @Component({ selector: 'cnsl-accounts-card', templateUrl: './accounts-card.component.html', styleUrls: ['./accounts-card.component.scss'], }) -export class AccountsCardComponent implements OnInit { - @Input() public user?: User.AsObject; - @Input() public iamuser: boolean | null = false; - - @Output() public closedCard: EventEmitter = new EventEmitter(); - public sessions: Session.AsObject[] = []; - public loadingUsers: boolean = false; - public UserState: any = UserState; - private labelpolicy = toSignal(this.userService.labelpolicy$, { initialValue: undefined }); - - constructor( - public authService: AuthenticationService, - private router: Router, - private userService: GrpcAuthService, - ) { - this.userService - .listMyUserSessions() - .then((sessions) => { - this.sessions = sessions.resultList.filter((user) => user.loginName !== this.user?.preferredLoginName); - this.loadingUsers = false; - }) - .catch(() => { - this.loadingUsers = false; - }); +export class AccountsCardComponent { + @Input({ required: true }) + public set user(user: User.AsObject) { + this.user$.next(user); } - ngOnInit(): void { - this.loadingUsers = true; + @Input() public iamuser: boolean | null = false; + + @Output() public closedCard = new EventEmitter(); + + protected readonly user$ = new ReplaySubject(1); + protected readonly UserState = UserState; + private readonly labelpolicy = toSignal(this.userService.labelpolicy$, { initialValue: undefined }); + protected readonly sessions$: Observable; + + constructor( + protected readonly authService: AuthenticationService, + private readonly router: Router, + private readonly userService: GrpcAuthService, + private readonly sessionService: SessionService, + private readonly featureService: NewFeatureService, + private readonly toast: ToastService, + ) { + this.sessions$ = this.getSessions().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + } + + private getUseLoginV2() { + return defer(() => this.featureService.getInstanceFeatures()).pipe( + map(({ loginV2 }) => loginV2?.required ?? false), + timeout(1000), + catchError((err) => { + if (!(err instanceof TimeoutError)) { + this.toast.showError(err); + } + return of(false); + }), + ); + } + + private getSessions(): Observable { + const useLoginV2$ = this.getUseLoginV2(); + + return useLoginV2$.pipe( + switchMap((useLoginV2) => { + if (useLoginV2) { + return this.getV2Sessions(); + } else { + return this.getV1Sessions(); + } + }), + catchError((err) => { + this.toast.showError(err); + return of([]); + }), + ); + } + + private getV1Sessions(): Observable { + return defer(() => this.userService.listMyUserSessions()).pipe( + mergeMap(({ resultList }) => from(resultList)), + withLatestFrom(this.user$), + filter(([{ loginName }, user]) => loginName !== user.preferredLoginName), + map(([s]) => ({ + displayName: s.displayName, + avatarUrl: s.avatarUrl, + loginName: s.loginName, + authState: s.authState, + userName: s.userName, + })), + toArray(), + ); + } + + private getV2Sessions(): Observable { + return defer(() => + this.sessionService.listSessions({ + queries: [ + { + query: { + case: 'userAgentQuery', + value: {}, + }, + }, + ], + }), + ).pipe( + mergeMap(({ sessions }) => from(sessions)), + withLatestFrom(this.user$), + filter(([s, user]) => s.factors?.user?.loginName !== user.preferredLoginName), + map(([s]) => ({ + displayName: s.factors?.user?.displayName ?? '', + avatarUrl: '', + loginName: s.factors?.user?.loginName ?? '', + authState: V2SessionState.ACTIVE, + userName: s.factors?.user?.loginName ?? '', + })), + map((s) => [s.loginName, s] as const), + toArray(), + map((sessions) => Array.from(new Map(sessions).values())), // Ensure unique loginNames + ); } public editUserProfile(): void { - this.router.navigate(['users/me']); + this.router.navigate(['users/me']).then(); this.closedCard.emit(); } - public closeCard(element: HTMLElement): void { - if (!element.classList.contains('dontcloseonclick')) { - this.closedCard.emit(); - } - } - public selectAccount(loginHint: string): void { const configWithPrompt: Partial = { customQueryParams: { login_hint: loginHint, }, }; - this.authService.authenticate(configWithPrompt); + this.authService.authenticate(configWithPrompt).then(); } public selectNewAccount(): void { @@ -67,7 +161,7 @@ export class AccountsCardComponent implements OnInit { prompt: 'login', } as any, }; - this.authService.authenticate(configWithPrompt); + this.authService.authenticate(configWithPrompt).then(); } public logout(): void { diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html new file mode 100644 index 0000000000..7948ba7554 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html @@ -0,0 +1,69 @@ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'ACTIONSTWO.EXECUTION.TABLE.CONDITION' | translate }} + + {{ row.execution.condition | condition }} + + {{ 'ACTIONSTWO.EXECUTION.TABLE.TYPE' | translate }} + {{ 'ACTIONSTWO.EXECUTION.TYPES.' + row.execution.condition.conditionType.case | translate }} + {{ 'ACTIONSTWO.EXECUTION.TABLE.TARGET' | translate }} +
+ + {{ target.name }} + +
+
+ {{ 'ACTIONSTWO.EXECUTION.TABLE.CREATIONDATE' | translate }} + + {{ row.execution.creationDate | timestampToDate | localizedDate: 'regular' }} + + + + +
+
+
diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.scss b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.scss new file mode 100644 index 0000000000..041332a88b --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.scss @@ -0,0 +1,12 @@ +.target-key { + display: flex; + white-space: nowrap; +} + +.icon { + font-size: 14px; + height: 14px; + width: 14px; + margin-right: 0.5rem; + margin-left: -0.5rem; +} diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts new file mode 100644 index 0000000000..af9673dbf5 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts @@ -0,0 +1,103 @@ +import { ChangeDetectionStrategy, Component, computed, effect, EventEmitter, Input, Output } from '@angular/core'; +import { combineLatestWith, Observable, ReplaySubject } from 'rxjs'; +import { filter, map, startWith } from 'rxjs/operators'; +import { MatTableDataSource } from '@angular/material/table'; +import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { CorrectlyTypedExecution } from '../../actions-two-add-action/actions-two-add-action-dialog.component'; + +@Component({ + selector: 'cnsl-actions-two-actions-table', + templateUrl: './actions-two-actions-table.component.html', + styleUrls: ['./actions-two-actions-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ActionsTwoActionsTableComponent { + @Output() + public readonly refresh = new EventEmitter(); + + @Output() + public readonly selected = new EventEmitter(); + + @Output() + public readonly delete = new EventEmitter(); + + @Input({ required: true }) + public set executions(executions: CorrectlyTypedExecution[] | null) { + this.executions$.next(executions); + } + + @Input({ required: true }) + public set targets(targets: Target[] | null) { + this.targets$.next(targets); + } + + private readonly executions$ = new ReplaySubject(1); + + private readonly targets$ = new ReplaySubject(1); + + protected readonly dataSource = this.getDataSource(); + + protected readonly loading = this.getLoading(); + + private getDataSource() { + const executions$: Observable = this.executions$.pipe(filter(Boolean), startWith([])); + const executionsSignal = toSignal(executions$, { requireSync: true }); + + const targetsMapSignal = this.getTargetsMap(); + + const dataSignal = computed(() => { + const executions = executionsSignal(); + const targetsMap = targetsMapSignal(); + + if (targetsMap.size === 0) { + return []; + } + + return executions.map((execution) => { + const mappedTargets = execution.targets + .map((target) => targetsMap.get(target)) + .filter((target): target is NonNullable => !!target); + return { execution, mappedTargets }; + }); + }); + + const dataSource = new MatTableDataSource(dataSignal()); + + effect(() => { + const data = dataSignal(); + if (dataSource.data !== data) { + dataSource.data = data; + } + }); + + return dataSource; + } + + private getTargetsMap() { + const targets$ = this.targets$.pipe(filter(Boolean), startWith([] as Target[])); + const targetsSignal = toSignal(targets$, { requireSync: true }); + + return computed(() => { + const map = new Map(); + for (const target of targetsSignal()) { + map.set(target.id, target); + } + return map; + }); + } + + private getLoading() { + const loading$ = this.executions$.pipe( + combineLatestWith(this.targets$), + map(([executions, targets]) => executions === null || targets === null), + startWith(true), + ); + + return toSignal(loading$, { requireSync: true }); + } + + protected trackTarget(_: number, target: Target) { + return target.id; + } +} diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html new file mode 100644 index 0000000000..3e6c31fc0e --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html @@ -0,0 +1,17 @@ +

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+

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

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

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

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

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

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

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

+ +

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

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

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

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

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

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

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

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

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

- + diff --git a/console/src/app/modules/form-field/validators/validators.ts b/console/src/app/modules/form-field/validators/validators.ts index f5f078433a..1ff592b11e 100644 --- a/console/src/app/modules/form-field/validators/validators.ts +++ b/console/src/app/modules/form-field/validators/validators.ts @@ -24,6 +24,17 @@ export function requiredValidator(c: AbstractControl): ValidationErrors | null { return i18nErr(Validators.required(c), 'ERRORS.REQUIRED'); } +export function atLeastOneFieldValidator(fields: string[]): ValidatorFn { + return (formGroup: AbstractControl): ValidationErrors | null => { + const isValid = fields.some((field) => { + const control = formGroup.get(field); + return control && control.value; + }); + + return isValid ? null : { atLeastOneRequired: true }; // Return an error if none are set + }; +} + export function minArrayLengthValidator(minArrLength: number): ValidatorFn { return (c: AbstractControl): ValidationErrors | null => { return arrayLengthValidator(c, minArrLength, 'ERRORS.ATLEASTONE'); diff --git a/console/src/app/modules/header/header.component.ts b/console/src/app/modules/header/header.component.ts index e560237343..c12c38e43a 100644 --- a/console/src/app/modules/header/header.component.ts +++ b/console/src/app/modules/header/header.component.ts @@ -15,7 +15,7 @@ import { ActionKeysType } from '../action-keys/action-keys.component'; }) export class HeaderComponent { @Input() public isDarkTheme: boolean = true; - @Input() public user?: User.AsObject; + @Input({ required: true }) public user!: User.AsObject; public showOrgContext: boolean = false; @Input() public org!: Org.AsObject; diff --git a/console/src/app/modules/iam-views/iam-views.component.html b/console/src/app/modules/iam-views/iam-views.component.html index 8669602665..f04053a521 100644 --- a/console/src/app/modules/iam-views/iam-views.component.html +++ b/console/src/app/modules/iam-views/iam-views.component.html @@ -1,7 +1,7 @@

{{ 'DESCRIPTIONS.SETTINGS.IAM_VIEWS.TITLE' | translate }}

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

- +
{{ 'IAM.FAILEDEVENTS.VIEWNAME' | translate }}
diff --git a/console/src/app/modules/idp-table/idp-table.component.html b/console/src/app/modules/idp-table/idp-table.component.html index 7188040897..d67659eafc 100644 --- a/console/src/app/modules/idp-table/idp-table.component.html +++ b/console/src/app/modules/idp-table/idp-table.component.html @@ -1,7 +1,6 @@ diff --git a/console/src/app/modules/members-table/members-table.component.html b/console/src/app/modules/members-table/members-table.component.html index 3b2e70cff7..c3a0b659e2 100644 --- a/console/src/app/modules/members-table/members-table.component.html +++ b/console/src/app/modules/members-table/members-table.component.html @@ -1,7 +1,6 @@
{{ 'IAM.VIEWS.VIEWNAME' | translate }}
+ + + + + + + + + + + + + + + +
+ {{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.PREVIOUS_TABLE.DEACTIVATED_ON' | translate }} + + {{ row.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }} + {{ 'APP.PAGES.ID' | translate }} + {{ row.id }} + {{ 'PROJECT.TYPE.TITLE' | translate }} + {{ row.key.case | uppercase }} +
+
+
+ diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-inactive-table/oidc-webkeys-inactive-table.component.ts b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-inactive-table/oidc-webkeys-inactive-table.component.ts new file mode 100644 index 0000000000..b2236ae673 --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-inactive-table/oidc-webkeys-inactive-table.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ReplaySubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { MatTableDataSource } from '@angular/material/table'; +import { WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb'; + +@Component({ + selector: 'cnsl-oidc-webkeys-inactive-table', + templateUrl: './oidc-webkeys-inactive-table.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OidcWebKeysInactiveTableComponent { + @Input({ required: true }) + public set inactiveWebKeys(webKeys: WebKey[] | null) { + this.inactiveWebKeys$.next(webKeys); + } + + private inactiveWebKeys$ = new ReplaySubject(1); + protected dataSource$ = this.inactiveWebKeys$.pipe( + filter(Boolean), + map((webKeys) => new MatTableDataSource(webKeys)), + ); +} diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.html b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.html new file mode 100644 index 0000000000..abaf656e2a --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.html @@ -0,0 +1,57 @@ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
{{ 'APP.PAGES.STATE' | translate }} + + {{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.ACTIVE' | translate }} + {{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.NEXT' | translate }} + {{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.FUTURE' | translate }} + + {{ 'APP.PAGES.ID' | translate }} + {{ row.id }} + {{ 'PROJECT.TYPE.TITLE' | translate }} + {{ row.key.case | uppercase }} + + + + +
+
+
diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.scss b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.scss new file mode 100644 index 0000000000..2ec90938a6 --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.scss @@ -0,0 +1,4 @@ +.state.next { + color: #0e6245; + border: 1px solid #0e6245; +} diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.ts b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.ts new file mode 100644 index 0000000000..c0be5c5d56 --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.ts @@ -0,0 +1,31 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { ReplaySubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { MatTableDataSource } from '@angular/material/table'; +import { State, WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb'; + +@Component({ + selector: 'cnsl-oidc-webkeys-table', + templateUrl: './oidc-webkeys-table.component.html', + styleUrls: ['./oidc-webkeys-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OidcWebKeysTableComponent { + @Output() + public readonly refresh = new EventEmitter(); + + @Output() + public readonly delete = new EventEmitter(); + + @Input({ required: true }) + public set webKeys(webKeys: WebKey[] | null) { + this.webKeys$.next(webKeys); + } + + private readonly webKeys$ = new ReplaySubject(1); + protected readonly dataSource$ = this.webKeys$.pipe( + filter(Boolean), + map((keys) => new MatTableDataSource(keys)), + ); + protected readonly State = State; +} diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.html b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.html new file mode 100644 index 0000000000..b34ee9d30b --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.html @@ -0,0 +1,19 @@ +

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

+

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

+

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

+

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

+

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

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

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

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

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

{{ title }}

-

{{ description }}

- - - - {{ setting.groupI18nKey | translate }} + + {{ setting.groupI18nKey | translate }} - - - - - +
diff --git a/console/src/app/modules/sidenav/sidenav.component.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 e07c82dc51..4b73491f08 100644 --- a/console/src/app/modules/sidenav/sidenav.component.ts +++ b/console/src/app/modules/sidenav/sidenav.component.ts @@ -1,7 +1,5 @@ -import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ChangeDetectionStrategy, Component, effect, EventEmitter, Input, Output, signal } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; - import { PolicyComponentServiceType } from '../policies/policy-component-types.enum'; export interface SidenavSetting { @@ -13,67 +11,68 @@ export interface SidenavSetting { [PolicyComponentServiceType.ADMIN]?: string[]; }; showWarn?: boolean; + beta?: boolean; } @Component({ selector: 'cnsl-sidenav', templateUrl: './sidenav.component.html', styleUrls: ['./sidenav.component.scss'], - providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SidenavComponent), multi: true }], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SidenavComponent implements ControlValueAccessor { - @Input() public title: string = ''; - @Input() public description: string = ''; +export class SidenavComponent { + @Input() public navigate: boolean = true; @Input() public indented: boolean = false; - @Input() public currentSetting?: string | undefined = undefined; - @Input() public settingsList: SidenavSetting[] = []; - @Input() public queryParam: string = ''; + @Input({ required: true }) public settingsList: SidenavSetting[] = []; + @Input({ required: true }) + public set setting(setting: SidenavSetting | null) { + if (!setting) { + return; + } + this.setting$.set(setting); + } + + @Output() + public settingChange = new EventEmitter(); + + protected readonly setting$ = signal(null); + + protected PolicyComponentServiceType = PolicyComponentServiceType; - public PolicyComponentServiceType: any = PolicyComponentServiceType; constructor( - private router: Router, - private route: ActivatedRoute, - ) {} + private readonly router: Router, + private readonly route: ActivatedRoute, + ) { + effect( + () => { + const setting = this.setting$(); + if (setting === null) { + return; + } - private onChange = (current: string | undefined) => {}; - private onTouch = (current: string | undefined) => {}; + this.settingChange.emit(setting); - @Input() get value(): string | undefined { - return this.currentSetting; + if (!this.navigate) { + return; + } + + this.router + .navigate([], { + relativeTo: this.route, + queryParams: { + id: setting ? setting.id : undefined, + }, + replaceUrl: true, + queryParamsHandling: 'merge', + skipLocationChange: false, + }) + .then(); + }, + { allowSignalWrites: true }, + ); } - set value(setting: string | undefined) { - this.currentSetting = setting; - - if (setting || setting === undefined || setting === '') { - this.onChange(setting); - this.onTouch(setting); - } - - if (this.queryParam && setting) { - this.router - .navigate([], { - relativeTo: this.route, - queryParams: { - [this.queryParam]: setting, - }, - replaceUrl: true, - queryParamsHandling: 'merge', - skipLocationChange: false, - }) - .then(); - } - } - - public writeValue(value: any) { - this.value = value; - } - - public registerOnChange(fn: any) { - this.onChange = fn; - } - - public registerOnTouched(fn: any) { - this.onTouch = fn; + protected trackSettings(_: number, setting: SidenavSetting): string { + return setting.id; } } diff --git a/console/src/app/modules/smtp-table/smtp-table.component.html b/console/src/app/modules/smtp-table/smtp-table.component.html index b71a7fcefd..3143aee793 100644 --- a/console/src/app/modules/smtp-table/smtp-table.component.html +++ b/console/src/app/modules/smtp-table/smtp-table.component.html @@ -1,7 +1,6 @@
+ + {{ '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/instance/instance-routing.module.ts b/console/src/app/pages/instance/instance-routing.module.ts index ca03086e85..1ad1cc442f 100644 --- a/console/src/app/pages/instance/instance-routing.module.ts +++ b/console/src/app/pages/instance/instance-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { AuthGuard } from 'src/app/guards/auth.guard'; -import { RoleGuard } from 'src/app/guards/role.guard'; +import { authGuard } from 'src/app/guards/auth.guard'; +import { roleGuard } from 'src/app/guards/role-guard'; import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum'; import { InstanceComponent } from './instance.component'; @@ -10,22 +10,28 @@ const routes: Routes = [ { path: '', component: InstanceComponent, - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['iam.read'], }, + children: [ + { + path: 'actions', + loadChildren: () => import('src/app/modules/actions-two/actions-two.module'), + }, + ], }, { path: 'members', loadChildren: () => import('./instance-members/instance-members.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['iam.member.read'], }, }, { path: 'provider', - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], loadChildren: () => import('src/app/modules/providers/providers.module'), data: { roles: ['iam.idp.read'], @@ -34,7 +40,7 @@ const routes: Routes = [ }, { path: 'smtpprovider', - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], loadChildren: () => import('src/app/modules/smtp-provider/smtp-provider.module'), data: { roles: ['iam.idp.read'], diff --git a/console/src/app/pages/instance/instance.component.html b/console/src/app/pages/instance/instance.component.html index 79b92773fd..c73881f168 100644 --- a/console/src/app/pages/instance/instance.component.html +++ b/console/src/app/pages/instance/instance.component.html @@ -40,12 +40,10 @@
- - + + + +
diff --git a/console/src/app/pages/instance/instance.component.ts b/console/src/app/pages/instance/instance.component.ts index 52151a2fa4..e52cdd7198 100644 --- a/console/src/app/pages/instance/instance.component.ts +++ b/console/src/app/pages/instance/instance.component.ts @@ -1,8 +1,8 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, DestroyRef } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Params, Router } from '@angular/router'; -import { BehaviorSubject, from, Observable, of, Subject } from 'rxjs'; -import { catchError, finalize, map, takeUntil } from 'rxjs/operators'; +import { BehaviorSubject, defer, from, Observable, of, shareReplay, TimeoutError } from 'rxjs'; +import { catchError, finalize, map, timeout } from 'rxjs/operators'; import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum'; import { InstanceDetail, State } from 'src/app/proto/generated/zitadel/instance_pb'; @@ -24,6 +24,7 @@ import { MESSAGETEXTS, NOTIFICATIONS, OIDC, + WEBKEYS, PRIVACYPOLICY, SECRETS, SECURITY, @@ -34,28 +35,33 @@ import { EVENTS, ORGANIZATIONS, FEATURESETTINGS, -} from '../../modules/settings-list/settings'; + ACTIONS, + ACTIONS_TARGETS, +} from 'src/app/modules/settings-list/settings'; 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'; @Component({ selector: 'cnsl-instance', templateUrl: './instance.component.html', styleUrls: ['./instance.component.scss'], }) -export class InstanceComponent implements OnInit, OnDestroy { - public instance?: InstanceDetail.AsObject; - public PolicyComponentServiceType: any = PolicyComponentServiceType; - private loadingSubject: BehaviorSubject = new BehaviorSubject(false); - public loading$: Observable = this.loadingSubject.asObservable(); - public totalMemberResult: number = 0; - public membersSubject: BehaviorSubject = new BehaviorSubject([]); - public State: any = State; +export class InstanceComponent { + protected instance?: InstanceDetail.AsObject; + protected readonly PolicyComponentServiceType = PolicyComponentServiceType; + private readonly loadingSubject: BehaviorSubject = new BehaviorSubject(false); + protected readonly loading$: Observable = this.loadingSubject.asObservable(); + protected totalMemberResult: number = 0; + protected readonly membersSubject: BehaviorSubject = new BehaviorSubject([]); + protected readonly State = State; - public id: string = ''; - public defaultSettingsList: SidenavSetting[] = [ + protected id: string = ''; + protected readonly defaultSettingsList: SidenavSetting[] = [ ORGANIZATIONS, FEATURESETTINGS, + ACTIONS, + ACTIONS_TARGETS, // notifications // { showWarn: true, ...NOTIFICATIONS }, NOTIFICATIONS, @@ -67,7 +73,6 @@ export class InstanceComponent implements OnInit, OnDestroy { COMPLEXITY, AGE, LOCKOUT, - DOMAIN, // appearance BRANDING, @@ -81,23 +86,24 @@ export class InstanceComponent implements OnInit, OnDestroy { PRIVACYPOLICY, LANGUAGES, OIDC, + WEBKEYS, SECRETS, SECURITY, ]; - public settingsList: Observable = of([]); - public customerPortalLink$ = this.envService.env.pipe(map((env) => env.customer_portal)); + protected readonly settingsList$: Observable; + protected readonly customerPortalLink$ = this.envService.env.pipe(map((env) => env.customer_portal)); - private destroy$: Subject = new Subject(); constructor( - public adminService: AdminService, - private dialog: MatDialog, - private toast: ToastService, + protected readonly adminService: AdminService, + private readonly dialog: MatDialog, + private readonly toast: ToastService, breadcrumbService: BreadcrumbService, - private router: Router, - private authService: GrpcAuthService, - private envService: EnvironmentService, + private readonly router: Router, + private readonly authService: GrpcAuthService, + private readonly envService: EnvironmentService, activatedRoute: ActivatedRoute, + private readonly destroyRef: DestroyRef, ) { this.loadMembers(); @@ -106,7 +112,6 @@ export class InstanceComponent implements OnInit, OnDestroy { name: 'Instance', routerLink: ['/instance'], }); - breadcrumbService.setBreadcrumb([instanceBread]); this.adminService @@ -120,12 +125,18 @@ export class InstanceComponent implements OnInit, OnDestroy { this.toast.showError(error); }); - activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params: Params) => { - const { id } = params; + activatedRoute.queryParamMap.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => { + const id = params.get('id'); if (id) { this.id = id; } }); + + this.settingsList$ = this.getSettingsList(); + } + + private getSettingsList(): Observable { + return this.authService.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin || []); } public loadMembers(): void { @@ -185,18 +196,6 @@ export class InstanceComponent implements OnInit, OnDestroy { } public showDetail(): void { - this.router.navigate(['/instance', 'members']); - } - - ngOnInit(): void { - this.settingsList = this.authService.isAllowedMapper( - this.defaultSettingsList, - (setting) => setting.requiredRoles.admin || [], - ); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + this.router.navigate(['/instance', 'members']).then(); } } diff --git a/console/src/app/pages/org-create/org-create.component.ts b/console/src/app/pages/org-create/org-create.component.ts index a82e04d2b5..fc1fc65ee8 100644 --- a/console/src/app/pages/org-create/org-create.component.ts +++ b/console/src/app/pages/org-create/org-create.component.ts @@ -1,27 +1,21 @@ import { animate, style, transition, trigger } from '@angular/animations'; import { Location } from '@angular/common'; import { Component } from '@angular/core'; -import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { Router } from '@angular/router'; -import { - containsLowerCaseValidator, - containsNumberValidator, - containsSymbolValidator, - containsUpperCaseValidator, - minLengthValidator, - passwordConfirmValidator, - requiredValidator, -} from 'src/app/modules/form-field/validators/validators'; +import { passwordConfirmValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators'; import { SetUpOrgRequest } from 'src/app/proto/generated/zitadel/admin_pb'; -import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; import { Gender } from 'src/app/proto/generated/zitadel/user_pb'; import { AdminService } from 'src/app/services/admin.service'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; -import { LanguagesService } from '../../services/languages.service'; +import { LanguagesService } from 'src/app/services/languages.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; +import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb'; +import { NewMgmtService } from 'src/app/services/new-mgmt.service'; +import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service'; @Component({ selector: 'cnsl-org-create', @@ -48,20 +42,22 @@ export class OrgCreateComponent { public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED]; - public policy?: PasswordComplexityPolicy.AsObject; + public policy?: PasswordComplexityPolicy; public usePassword: boolean = false; public forSelf: boolean = true; constructor( - private router: Router, - private toast: ToastService, - private adminService: AdminService, - private _location: Location, - private fb: UntypedFormBuilder, - private mgmtService: ManagementService, - private authService: GrpcAuthService, - public langSvc: LanguagesService, + private readonly router: Router, + private readonly toast: ToastService, + private readonly adminService: AdminService, + private readonly _location: Location, + private readonly fb: UntypedFormBuilder, + private readonly mgmtService: ManagementService, + private readonly newMgmtService: NewMgmtService, + private readonly authService: GrpcAuthService, + private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService, + public readonly langSvc: LanguagesService, breadcrumbService: BreadcrumbService, ) { const instanceBread = new Breadcrumb({ @@ -103,8 +99,8 @@ export class OrgCreateComponent { this.adminService .SetUpOrg(createOrgRequest, humanRequest) .then(() => { - this.authService.revalidateOrgs(); - this.router.navigate(['/orgs']); + this.authService.revalidateOrgs().then(); + this.router.navigate(['/orgs']).then(); }) .catch((error) => { this.toast.showError(error); @@ -133,36 +129,12 @@ export class OrgCreateComponent { } public initPwdValidators(): void { - const validators: Validators[] = [requiredValidator]; - if (this.usePassword) { - this.mgmtService.getDefaultPasswordComplexityPolicy().then((data) => { - if (data.policy) { - this.policy = data.policy; - - if (this.policy.minLength) { - validators.push(minLengthValidator(this.policy.minLength)); - } - if (this.policy.hasLowercase) { - validators.push(containsLowerCaseValidator); - } - if (this.policy.hasUppercase) { - validators.push(containsUpperCaseValidator); - } - if (this.policy.hasNumber) { - validators.push(containsNumberValidator); - } - if (this.policy.hasSymbol) { - validators.push(containsSymbolValidator); - } - - const pwdValidators = [...validators] as ValidatorFn[]; - const confirmPwdValidators = [requiredValidator, passwordConfirmValidator()] as ValidatorFn[]; - this.pwdForm = this.fb.group({ - password: ['', pwdValidators], - confirmPassword: ['', confirmPwdValidators], - }); - } + this.newMgmtService.getDefaultPasswordComplexityPolicy().then((data) => { + this.pwdForm = this.fb.group({ + password: ['', this.passwordComplexityValidatorFactory.buildValidators(data.policy)], + confirmPassword: ['', [requiredValidator, passwordConfirmValidator()]], + }); }); } else { this.pwdForm = this.fb.group({ @@ -194,8 +166,8 @@ export class OrgCreateComponent { this.mgmtService .addOrg(this.name.value) .then(() => { - this.authService.revalidateOrgs(); - this.router.navigate(['/orgs']); + this.authService.revalidateOrgs().then(); + this.router.navigate(['/orgs']).then(); }) .catch((error) => { this.toast.showError(error); diff --git a/console/src/app/pages/orgs/org-routing.module.ts b/console/src/app/pages/orgs/org-routing.module.ts index 0f79b185e7..1ce6b0049d 100644 --- a/console/src/app/pages/orgs/org-routing.module.ts +++ b/console/src/app/pages/orgs/org-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { AuthGuard } from 'src/app/guards/auth.guard'; -import { RoleGuard } from 'src/app/guards/role.guard'; +import { authGuard } from 'src/app/guards/auth.guard'; +import { roleGuard } from 'src/app/guards/role-guard'; import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum'; import { OrgDetailComponent } from './org-detail/org-detail.component'; @@ -13,7 +13,7 @@ const routes: Routes = [ }, { path: 'provider', - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], loadChildren: () => import('src/app/modules/providers/providers.module'), data: { roles: ['org.idp.read'], diff --git a/console/src/app/pages/projects/apps/app-detail/app-detail.component.html b/console/src/app/pages/projects/apps/app-detail/app-detail.component.html index 4c2d2cfb82..082ca01eed 100644 --- a/console/src/app/pages/projects/apps/app-detail/app-detail.component.html +++ b/console/src/app/pages/projects/apps/app-detail/app-detail.component.html @@ -1,6 +1,6 @@
-
+
- - @@ -80,8 +75,8 @@
- - + +
@@ -239,6 +234,26 @@ >
+ +
+
+ +
+ diff --git a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.scss b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.scss new file mode 100644 index 0000000000..7a29364fc3 --- /dev/null +++ b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.scss @@ -0,0 +1,57 @@ +.content { + max-width: 45rem; + + @media only screen and (max-width: 500px) { + padding: 0 0.5rem; + } +} + +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto; + grid-template-areas: + 'email email' + 'emailVerified emailVerified' + 'username username' + 'givenName familyName' + 'authenticationFactor authenticationFactor'; + column-gap: 1rem; +} + +.email { + grid-area: email; +} + +.emailVerified { + grid-area: emailVerified; +} + +.givenName { + grid-area: givenName; +} + +.familyName { + grid-area: familyName; +} + +.username { + grid-area: username; +} + +.authenticationFactor { + grid-area: authenticationFactor; + margin-bottom: 1rem; +} + +.authenticationFactorRadioGroup > mat-radio-button { + display: block; +} + +.authenticationFactorButton { + margin-top: 1rem; +} + +.stretchInput { + max-width: unset; +} diff --git a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts new file mode 100644 index 0000000000..9fd765264d --- /dev/null +++ b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts @@ -0,0 +1,257 @@ +import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, signal } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ToastService } from 'src/app/services/toast.service'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { UserService } from 'src/app/services/user.service'; +import { Location } from '@angular/common'; +import { + emailValidator, + minLengthValidator, + passwordConfirmValidator, + requiredValidator, +} from 'src/app/modules/form-field/validators/validators'; +import { NewMgmtService } from 'src/app/services/new-mgmt.service'; +import { + defaultIfEmpty, + defer, + EMPTY, + firstValueFrom, + mergeWith, + NEVER, + Observable, + of, + shareReplay, + TimeoutError, +} from 'rxjs'; +import { catchError, filter, map, startWith, timeout } from 'rxjs/operators'; +import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { AddHumanUserRequestSchema } from '@zitadel/proto/zitadel/user/v2/user_service_pb'; +import { LoginV2FeatureFlag } from '@zitadel/proto/zitadel/feature/v2/feature_pb'; +import { withLatestFromSynchronousFix } from 'src/app/utils/withLatestFromSynchronousFix'; +import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service'; +import { NewFeatureService } from 'src/app/services/new-feature.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +type PwdForm = ReturnType; +type AuthenticationFactor = + | { factor: 'none' } + | { factor: 'initialPassword'; form: PwdForm; policy: PasswordComplexityPolicy } + | { factor: 'invitation' }; + +@Component({ + selector: 'cnsl-user-create-v2', + templateUrl: './user-create-v2.component.html', + styleUrls: ['./user-create-v2.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UserCreateV2Component implements OnInit { + protected readonly loading = signal(false); + + protected readonly userForm: ReturnType; + + private readonly passwordComplexityPolicy$: Observable; + protected readonly authenticationFactor$: Observable; + private readonly useLoginV2$: Observable; + + constructor( + private readonly router: Router, + private readonly toast: ToastService, + private readonly fb: FormBuilder, + private readonly userService: UserService, + private readonly newMgmtService: NewMgmtService, + private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService, + private readonly featureService: NewFeatureService, + private readonly destroyRef: DestroyRef, + private readonly route: ActivatedRoute, + protected readonly location: Location, + ) { + this.userForm = this.buildUserForm(); + + this.passwordComplexityPolicy$ = this.getPasswordComplexityPolicy().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.authenticationFactor$ = this.getAuthenticationFactor(this.userForm, this.passwordComplexityPolicy$); + this.useLoginV2$ = this.getUseLoginV2().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + } + + ngOnInit(): void { + this.useLoginV2$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(); + this.authenticationFactor$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async ({ factor }) => { + // preserve current factor choice when reloading helpful while developing + await this.router.navigate([], { + relativeTo: this.route, + queryParams: { + factor, + }, + queryParamsHandling: 'merge', + }); + }); + } + + public buildUserForm() { + const param = this.route.snapshot.queryParamMap.get('factor'); + const authenticationFactor = + param === 'none' ? param : param === 'initialPassword' ? param : param === 'invitation' ? param : 'none'; + + return this.fb.group({ + email: new FormControl('', { nonNullable: true, validators: [requiredValidator, emailValidator] }), + username: new FormControl('', { nonNullable: true, validators: [requiredValidator, minLengthValidator(2)] }), + givenName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + familyName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + emailVerified: new FormControl(false, { nonNullable: true }), + authenticationFactor: new FormControl(authenticationFactor, { + nonNullable: true, + }), + }); + } + + private getPasswordComplexityPolicy() { + return defer(() => this.newMgmtService.getPasswordComplexityPolicy()).pipe( + map(({ policy }) => policy), + filter(Boolean), + catchError((error) => { + this.toast.showError(error); + return EMPTY; + }), + ); + } + + private getAuthenticationFactor( + userForm: typeof this.userForm, + passwordComplexityPolicy$: Observable, + ): Observable { + const pwdForm$ = passwordComplexityPolicy$.pipe( + defaultIfEmpty(undefined), + map((policy) => this.buildPwdForm(policy)), + ); + + return userForm.controls.authenticationFactor.valueChanges.pipe( + startWith(userForm.controls.authenticationFactor.value), + withLatestFromSynchronousFix(pwdForm$, passwordComplexityPolicy$), + map(([factor, form, policy]) => { + if (factor === 'initialPassword') { + return { factor, form, policy }; + } + // reset emailVerified when we switch to invitation + if (factor === 'invitation') { + userForm.controls.emailVerified.setValue(false); + } + return { factor }; + }), + ); + } + + private buildPwdForm(policy: PasswordComplexityPolicy | undefined) { + return this.fb.group({ + password: new FormControl('', { + nonNullable: true, + validators: this.passwordComplexityValidatorFactory.buildValidators(policy), + }), + confirmPassword: new FormControl('', { + nonNullable: true, + validators: [requiredValidator, passwordConfirmValidator()], + }), + }); + } + + private getUseLoginV2() { + return defer(() => this.featureService.getInstanceFeatures()).pipe( + map(({ loginV2 }) => loginV2), + timeout(1000), + catchError((err) => { + if (!(err instanceof TimeoutError)) { + this.toast.showError(err); + } + return of(undefined); + }), + mergeWith(NEVER), + ); + } + + protected async createUserV2(authenticationFactor: AuthenticationFactor) { + try { + await this.createUserV2Try(authenticationFactor); + } catch (error) { + this.toast.showError(error); + } finally { + this.loading.set(false); + } + } + + private async createUserV2Try(authenticationFactor: AuthenticationFactor) { + this.loading.set(true); + + const userValues = this.userForm.getRawValue(); + + const humanReq: MessageInitShape = { + username: userValues.username, + profile: { + givenName: userValues.givenName, + familyName: userValues.familyName, + }, + email: { + email: userValues.email, + verification: { + case: 'isVerified', + value: userValues.emailVerified, + }, + }, + }; + + if (authenticationFactor.factor === 'initialPassword') { + const { password } = authenticationFactor.form.getRawValue(); + humanReq.passwordType = { + case: 'password', + value: { + password, + }, + }; + } + + const resp = await this.userService.addHumanUser(humanReq); + if (authenticationFactor.factor === 'invitation') { + const url = await this.getUrlTemplate(); + await this.userService.createInviteCode({ + userId: resp.userId, + verification: { + case: 'sendCode', + value: url + ? { + urlTemplate: `${url}verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`, + } + : {}, + }, + }); + } + + this.toast.showInfo('USER.TOAST.CREATED', true); + await this.router.navigate(['users', resp.userId], { queryParams: { new: true } }); + } + + private async getUrlTemplate() { + const useLoginV2 = await firstValueFrom(this.useLoginV2$); + if (!useLoginV2?.required) { + // loginV2 is not enabled + return undefined; + } + + const { baseUri } = useLoginV2; + // if base uri is not set, we use the default for the cloud hosted login v2 + if (!baseUri) { + return new URL(location.origin + '/ui/v2/login/'); + } + + const baseUriWithTrailingSlash = baseUri.endsWith('/') ? baseUri : `${baseUri}/`; + try { + // first we try to create a URL directly from the baseUri + return new URL(baseUriWithTrailingSlash); + } catch (_) { + // if this does not work we assume that the baseUri is relative, + // and we need to add the location.origin + // we make sure the relative url has a slash at the beginning and end + const baseUriWithSlashes = baseUriWithTrailingSlash.startsWith('/') + ? baseUriWithTrailingSlash + : `/${baseUriWithTrailingSlash}`; + return new URL(location.origin + baseUriWithSlashes); + } + } +} diff --git a/console/src/app/pages/users/user-create/user-create.component.html b/console/src/app/pages/users/user-create/user-create.component.html index 13fcb95bb3..1b65cc992e 100644 --- a/console/src/app/pages/users/user-create/user-create.component.html +++ b/console/src/app/pages/users/user-create/user-create.component.html @@ -1,4 +1,6 @@ + (1); - @ViewChild('suffix') public set suffix(suffix: ElementRef) { - this.suffix$.next(suffix.nativeElement); + @ViewChild('suffix') public set suffix(suffix: ElementRef | undefined) { + if (suffix?.nativeElement) { + this.suffix$.next(suffix.nativeElement); + } } protected usePassword: boolean = false; protected readonly envSuffix$: Observable; protected readonly userForm: ReturnType; protected readonly pwdForm$: ReturnType; - protected readonly passwordComplexityPolicy$: Observable; + protected readonly passwordComplexityPolicy$: Observable; + protected readonly useV2Api$: Observable; protected readonly suffixPadding$: Observable; constructor( @@ -59,24 +72,24 @@ export class UserCreateComponent implements OnInit { private readonly toast: ToastService, private readonly fb: FormBuilder, private readonly mgmtService: ManagementService, + private readonly newMgmtService: NewMgmtService, private readonly destroyRef: DestroyRef, private readonly breadcrumbService: BreadcrumbService, protected readonly location: Location, protected readonly langSvc: LanguagesService, + private readonly featureService: NewFeatureService, + private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService, countryCallingCodesService: CountryCallingCodesService, ) { this.envSuffix$ = this.getEnvSuffix(); this.suffixPadding$ = this.getSuffixPadding(); this.passwordComplexityPolicy$ = this.getPasswordComplexityPolicy().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.useV2Api$ = this.getUseV2Api().pipe(shareReplay({ refCount: true, bufferSize: 1 })); this.userForm = this.buildUserForm(); this.pwdForm$ = this.buildPwdForm(this.passwordComplexityPolicy$); this.countryPhoneCodes = countryCallingCodesService.getCountryCallingCodes(); - } - - ngOnInit(): void { - this.watchPhoneChanges(); this.breadcrumbService.setBreadcrumb([ new Breadcrumb({ @@ -86,6 +99,10 @@ export class UserCreateComponent implements OnInit { ]); } + ngOnInit(): void { + this.watchPhoneChanges(); + } + private getEnvSuffix() { const domainPolicy$ = defer(() => this.mgmtService.getDomainPolicy()); const orgDomains$ = defer(() => this.mgmtService.listOrgDomains()); @@ -112,7 +129,7 @@ export class UserCreateComponent implements OnInit { } private getPasswordComplexityPolicy() { - return defer(() => this.mgmtService.getPasswordComplexityPolicy()).pipe( + return defer(() => this.newMgmtService.getPasswordComplexityPolicy()).pipe( map(({ policy }) => policy), catchError((error) => { this.toast.showError(error); @@ -121,6 +138,19 @@ export class UserCreateComponent implements OnInit { ); } + private getUseV2Api() { + return defer(() => this.featureService.getInstanceFeatures()).pipe( + map((features) => features.consoleUseV2UserApi?.enabled ?? false), + timeout(1000), + catchError((err) => { + if (!(err instanceof TimeoutError)) { + this.toast.showError(err); + } + return of(false); + }), + ); + } + private buildUserForm() { return this.fb.group({ email: new FormControl('', { nonNullable: true, validators: [requiredValidator, emailValidator] }), @@ -135,27 +165,14 @@ export class UserCreateComponent implements OnInit { }); } - private buildPwdForm(passwordComplexityPolicy$: Observable) { + private buildPwdForm(passwordComplexityPolicy$: Observable) { return passwordComplexityPolicy$.pipe( map((policy) => { - const validators: ValidatorFn[] = [requiredValidator]; - if (policy?.minLength) { - validators.push(minLengthValidator(policy.minLength)); - } - if (policy?.hasLowercase) { - validators.push(containsLowerCaseValidator); - } - if (policy?.hasUppercase) { - validators.push(containsUpperCaseValidator); - } - if (policy?.hasNumber) { - validators.push(containsNumberValidator); - } - if (policy?.hasSymbol) { - validators.push(containsSymbolValidator); - } return this.fb.group({ - password: new FormControl('', { nonNullable: true, validators }), + password: new FormControl('', { + nonNullable: true, + validators: this.passwordComplexityValidatorFactory.buildValidators(policy), + }), confirmPassword: new FormControl('', { nonNullable: true, validators: [requiredValidator, passwordConfirmValidator()], diff --git a/console/src/app/pages/users/user-create/user-create.module.ts b/console/src/app/pages/users/user-create/user-create.module.ts index a5483f1802..5bab56f861 100644 --- a/console/src/app/pages/users/user-create/user-create.module.ts +++ b/console/src/app/pages/users/user-create/user-create.module.ts @@ -19,9 +19,11 @@ import { PasswordComplexityViewModule } from 'src/app/modules/password-complexit import { CountryCallingCodesService } from 'src/app/services/country-calling-codes.service'; import { UserCreateRoutingModule } from './user-create-routing.module'; import { UserCreateComponent } from './user-create.component'; +import { UserCreateV2Component } from './user-create-v2/user-create-v2.component'; +import { MatRadioModule } from '@angular/material/radio'; @NgModule({ - declarations: [UserCreateComponent], + declarations: [UserCreateComponent, UserCreateV2Component], providers: [CountryCallingCodesService], imports: [ UserCreateRoutingModule, @@ -42,6 +44,7 @@ import { UserCreateComponent } from './user-create.component'; DetailLayoutModule, InputModule, MatRippleModule, + MatRadioModule, ], }) export default class UserCreateModule {} diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.html index 44b8f83548..5f25ee873d 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.html @@ -11,12 +11,7 @@ > refresh - + - + - +
diff --git a/console/src/app/pages/users/user-detail/password/password.component.ts b/console/src/app/pages/users/user-detail/password/password.component.ts index b37d79c39b..ef18559e09 100644 --- a/console/src/app/pages/users/user-detail/password/password.component.ts +++ b/console/src/app/pages/users/user-detail/password/password.component.ts @@ -1,8 +1,7 @@ import { Component, DestroyRef, OnInit } from '@angular/core'; -import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { - take, map, switchMap, firstValueFrom, @@ -12,24 +11,18 @@ import { of, shareReplay, combineLatestWith, + EMPTY, } from 'rxjs'; -import { - containsLowerCaseValidator, - containsNumberValidator, - containsSymbolValidator, - containsUpperCaseValidator, - minLengthValidator, - passwordConfirmValidator, - requiredValidator, -} from 'src/app/modules/form-field/validators/validators'; -import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; +import { passwordConfirmValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; -import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { ToastService } from 'src/app/services/toast.service'; import { catchError, filter } from 'rxjs/operators'; -import { User } from 'src/app/proto/generated/zitadel/user_pb'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { UserService } from '../../../../services/user.service'; +import { UserService } from 'src/app/services/user.service'; +import { User } from '@zitadel/proto/zitadel/user/v2/user_pb'; +import { NewAuthService } from 'src/app/services/new-auth.service'; +import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb'; +import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service'; @Component({ selector: 'cnsl-password', @@ -41,34 +34,53 @@ export class PasswordComponent implements OnInit { protected readonly username$: Observable; protected readonly id$: Observable; protected readonly form$: Observable; - protected readonly passwordPolicy$: Observable; - protected readonly user$: Observable; + protected readonly passwordPolicy$: Observable; + protected readonly user$: Observable; constructor( - activatedRoute: ActivatedRoute, + private readonly activatedRoute: ActivatedRoute, private readonly fb: UntypedFormBuilder, - private readonly authService: GrpcAuthService, private readonly userService: UserService, + private readonly newAuthService: NewAuthService, private readonly toast: ToastService, private readonly breadcrumbService: BreadcrumbService, private readonly destroyRef: DestroyRef, + private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService, ) { - const usernameParam$ = activatedRoute.queryParamMap.pipe( - map((params) => params.get('username')), - filter(Boolean), - ); this.id$ = activatedRoute.paramMap.pipe(map((params) => params.get('id') ?? undefined)); - - this.user$ = this.authService.user.pipe(take(1), filter(Boolean)); - this.username$ = usernameParam$.pipe(mergeWith(this.user$.pipe(map((user) => user.preferredLoginName)))); - + this.user$ = this.getUser().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.username$ = this.getUsername(this.user$); this.breadcrumb$ = this.getBreadcrumb$(this.id$, this.user$); this.passwordPolicy$ = this.getPasswordPolicy$().pipe(shareReplay({ refCount: true, bufferSize: 1 })); - const validators$ = this.getValidators$(this.passwordPolicy$); - this.form$ = this.getForm$(this.id$, validators$); + this.form$ = this.getForm$(this.id$, this.passwordPolicy$); } - private getBreadcrumb$(id$: Observable, user$: Observable): Observable { + ngOnInit() { + this.breadcrumb$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((breadcrumbs) => { + this.breadcrumbService.setBreadcrumb(breadcrumbs); + }); + } + + private getUser() { + return this.userService.user$.pipe( + catchError((err) => { + this.toast.showError(err); + return EMPTY; + }), + ); + } + + private getUsername(user$: Observable) { + const prefferedLoginName$ = user$.pipe(map((user) => user.preferredLoginName)); + + return this.activatedRoute.queryParamMap.pipe( + map((params) => params.get('username')), + filter(Boolean), + mergeWith(prefferedLoginName$), + ); + } + + private getBreadcrumb$(id$: Observable, user$: Observable): Observable { return id$.pipe( switchMap(async (id) => { if (id) { @@ -86,7 +98,7 @@ export class PasswordComponent implements OnInit { return [ new Breadcrumb({ type: BreadcrumbType.AUTHUSER, - name: user.human?.profile?.displayName, + name: (user.type.case === 'human' && user.type.value.profile?.displayName) || undefined, routerLink: ['/users', 'me'], }), ]; @@ -94,39 +106,22 @@ export class PasswordComponent implements OnInit { ); } - private getValidators$( - passwordPolicy$: Observable, - ): Observable { - return passwordPolicy$.pipe( - map((policy) => { - const validators: Validators[] = [requiredValidator]; - if (!policy) { - return validators; - } - if (policy.minLength) { - validators.push(minLengthValidator(policy.minLength)); - } - if (policy.hasLowercase) { - validators.push(containsLowerCaseValidator); - } - if (policy.hasUppercase) { - validators.push(containsUpperCaseValidator); - } - if (policy.hasNumber) { - validators.push(containsNumberValidator); - } - if (policy.hasSymbol) { - validators.push(containsSymbolValidator); - } - return validators; + private getPasswordPolicy$(): Observable { + return defer(() => this.newAuthService.getMyPasswordComplexityPolicy()).pipe( + map((resp) => resp.policy), + catchError((err) => { + this.toast.showError(err); + return of(undefined); }), ); } private getForm$( id$: Observable, - validators$: Observable, + policy$: Observable, ): Observable { + const validators$ = policy$.pipe(map((policy) => this.passwordComplexityValidatorFactory.buildValidators(policy))); + return id$.pipe( combineLatestWith(validators$), map(([id, validators]) => { @@ -146,19 +141,6 @@ export class PasswordComponent implements OnInit { ); } - private getPasswordPolicy$(): Observable { - return defer(() => this.authService.getMyPasswordComplexityPolicy()).pipe( - map((resp) => resp.policy), - catchError(() => of(undefined)), - ); - } - - ngOnInit() { - this.breadcrumb$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((breadcrumbs) => { - this.breadcrumbService.setBreadcrumb(breadcrumbs); - }); - } - public async setInitialPassword(userId: string, form: UntypedFormGroup): Promise { const password = this.password(form)?.value; @@ -182,7 +164,7 @@ export class PasswordComponent implements OnInit { window.history.back(); } - public async setPassword(form: UntypedFormGroup, user: User.AsObject): Promise { + public async setPassword(form: UntypedFormGroup, user: User): Promise { const currentPassword = this.currentPassword(form); const newPassword = this.newPassword(form); @@ -192,7 +174,7 @@ export class PasswordComponent implements OnInit { try { await this.userService.setPassword({ - userId: user.id, + userId: user.userId, newPassword: { password: newPassword.value, changeRequired: false, diff --git a/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.html b/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.html index 507c0b929f..d0a3071696 100644 --- a/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.html +++ b/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.html @@ -11,7 +11,7 @@ > refresh - + - +
diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.html b/console/src/app/pages/users/user-list/user-table/user-table.component.html index 07583b5626..af293c01a4 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.html +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.html @@ -2,7 +2,6 @@ *ngIf="type$ | async as type" [loading]="loading()" (refreshed)="this.refresh$.next(true)" - [dataSize]="dataSize()" [hideRefresh]="true" [timestamp]="(users$ | async)?.details?.timestamp" [selection]="selection" diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.ts b/console/src/app/pages/users/user-list/user-table/user-table.component.ts index 3e4c32e030..6ff0d2cd67 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.ts +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.ts @@ -159,7 +159,7 @@ export class UserTableComponent implements OnInit { } private getMyUser() { - return defer(() => this.userService.getMyUser()).pipe( + return this.userService.user$.pipe( catchError((error) => { this.toast.showError(error); return EMPTY; diff --git a/console/src/app/pages/users/users-routing.module.ts b/console/src/app/pages/users/users-routing.module.ts index 240d32638e..73551a8cc1 100644 --- a/console/src/app/pages/users/users-routing.module.ts +++ b/console/src/app/pages/users/users-routing.module.ts @@ -1,8 +1,8 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { AuthGuard } from 'src/app/guards/auth.guard'; -import { RoleGuard } from 'src/app/guards/role.guard'; -import { UserGuard } from 'src/app/guards/user.guard'; +import { authGuard } from 'src/app/guards/auth.guard'; +import { roleGuard } from 'src/app/guards/role-guard'; +import { userGuard } from 'src/app/guards/user-guard'; import { Type } from 'src/app/proto/generated/zitadel/user_pb'; import { AuthUserDetailComponent } from './user-detail/auth-user-detail/auth-user-detail.component'; @@ -22,7 +22,7 @@ const routes: Routes = [ { path: 'create', loadChildren: () => import('./user-create/user-create.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['user.write'], }, @@ -30,7 +30,7 @@ const routes: Routes = [ { path: 'create-machine', loadChildren: () => import('./user-create-machine/user-create-machine.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['user.write'], }, @@ -38,7 +38,7 @@ const routes: Routes = [ { path: 'me', component: AuthUserDetailComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { animation: 'HomePage', }, @@ -46,13 +46,13 @@ const routes: Routes = [ { path: 'me/password', component: PasswordComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { animation: 'AddPage' }, }, { path: ':id', component: UserDetailComponent, - canActivate: [AuthGuard, UserGuard, RoleGuard], + canActivate: [authGuard, userGuard, roleGuard], data: { roles: ['user.read'], animation: 'HomePage', @@ -61,7 +61,7 @@ const routes: Routes = [ { path: ':id/password', component: PasswordComponent, - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['user.write'], animation: 'AddPage', diff --git a/console/src/app/pipes/action-condition-pipe/action-condition-pipe.module.ts b/console/src/app/pipes/action-condition-pipe/action-condition-pipe.module.ts new file mode 100644 index 0000000000..2af211d5bc --- /dev/null +++ b/console/src/app/pipes/action-condition-pipe/action-condition-pipe.module.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { ActionConditionPipe } from './action-condition-pipe.pipe'; + +@NgModule({ + declarations: [ActionConditionPipe], + imports: [CommonModule], + exports: [ActionConditionPipe], +}) +export class ActionConditionPipeModule {} diff --git a/console/src/app/pipes/action-condition-pipe/action-condition-pipe.pipe.ts b/console/src/app/pipes/action-condition-pipe/action-condition-pipe.pipe.ts new file mode 100644 index 0000000000..7c45f9da5f --- /dev/null +++ b/console/src/app/pipes/action-condition-pipe/action-condition-pipe.pipe.ts @@ -0,0 +1,29 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { Condition } from '@zitadel/proto/zitadel/action/v2beta/execution_pb'; + +@Pipe({ + name: 'condition', +}) +export class ActionConditionPipe implements PipeTransform { + transform(condition?: Condition): string { + if (!condition?.conditionType?.case) { + return ''; + } + + const conditionType = condition.conditionType.value; + + if ('name' in conditionType) { + // Applies for function condition + return `function: ${conditionType.name}`; + } + + const { condition: innerCondition } = conditionType; + + if (typeof innerCondition.value === 'string') { + // Applies for service, method condition of Request/ResponseCondition, event, and group of EventCondition + return `${innerCondition.case}: ${innerCondition.value}`; + } + + return `all`; + } +} diff --git a/console/src/app/pipes/timestamp-to-date-pipe/timestamp-to-date.pipe.ts b/console/src/app/pipes/timestamp-to-date-pipe/timestamp-to-date.pipe.ts index 8ec703230d..8fadbd30ed 100644 --- a/console/src/app/pipes/timestamp-to-date-pipe/timestamp-to-date.pipe.ts +++ b/console/src/app/pipes/timestamp-to-date-pipe/timestamp-to-date.pipe.ts @@ -1,18 +1,15 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { Timestamp as ConnectTimestamp } from '@bufbuild/protobuf/wkt'; +import { Timestamp as BufTimestamp } from '@bufbuild/protobuf/wkt'; import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb'; @Pipe({ name: 'timestampToDate', }) export class TimestampToDatePipe implements PipeTransform { - transform(value: ConnectTimestamp | Timestamp.AsObject, ...args: unknown[]): unknown { - return this.dateFromTimestamp(value); - } - - private dateFromTimestamp(date: ConnectTimestamp | Timestamp.AsObject): any { - if (date?.seconds !== undefined && date?.nanos !== undefined) { + transform(date: BufTimestamp | Timestamp.AsObject | undefined): Date | undefined { + if (date?.seconds && date.nanos) { return new Date(Number(date.seconds) * 1000 + date.nanos / 1000 / 1000); } + return undefined; } } diff --git a/console/src/app/services/action.service.ts b/console/src/app/services/action.service.ts new file mode 100644 index 0000000000..dabe2faf01 --- /dev/null +++ b/console/src/app/services/action.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@angular/core'; +import { GrpcService } from './grpc.service'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { + CreateTargetRequestSchema, + CreateTargetResponse, + DeleteTargetRequestSchema, + GetTargetRequestSchema, + GetTargetResponse, + ListExecutionFunctionsRequestSchema, + ListExecutionFunctionsResponse, + ListExecutionMethodsRequestSchema, + ListExecutionMethodsResponse, + ListExecutionServicesRequestSchema, + ListExecutionServicesResponse, + ListExecutionsRequestSchema, + ListExecutionsResponse, + ListTargetsRequestSchema, + ListTargetsResponse, + SetExecutionRequestSchema, + SetExecutionResponse, + UpdateTargetRequestSchema, + UpdateTargetResponse, +} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; + +@Injectable({ + providedIn: 'root', +}) +export class ActionService { + constructor(private readonly grpcService: GrpcService) {} + + public listTargets(req: MessageInitShape): Promise { + return this.grpcService.actionNew.listTargets(req); + } + + public createTarget(req: MessageInitShape): Promise { + return this.grpcService.actionNew.createTarget(req); + } + + public deleteTarget(req: MessageInitShape): Promise { + return this.grpcService.actionNew.deleteTarget(req); + } + + public getTarget(req: MessageInitShape): Promise { + return this.grpcService.actionNew.getTarget(req); + } + + public updateTarget(req: MessageInitShape): Promise { + return this.grpcService.actionNew.updateTarget(req); + } + + public listExecutionFunctions( + req: MessageInitShape, + ): Promise { + return this.grpcService.actionNew.listExecutionFunctions(req); + } + + public listExecutionMethods( + req: MessageInitShape, + ): Promise { + return this.grpcService.actionNew.listExecutionMethods(req); + } + + public listExecutionServices( + req: MessageInitShape, + ): Promise { + return this.grpcService.actionNew.listExecutionServices(req); + } + + public listExecutions(req: MessageInitShape): Promise { + return this.grpcService.actionNew.listExecutions(req); + } + + public setExecution(req: MessageInitShape): Promise { + return this.grpcService.actionNew.setExecution(req); + } +} diff --git a/console/src/app/services/feature.service.ts b/console/src/app/services/feature.service.ts deleted file mode 100644 index f009f165d4..0000000000 --- a/console/src/app/services/feature.service.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Injectable } from '@angular/core'; -import { GrpcService } from './grpc.service'; - -import { - GetOrganizationFeaturesRequest, - GetOrganizationFeaturesResponse, -} from '../proto/generated/zitadel/feature/v2beta/organization_pb'; -import { GetUserFeaturesRequest, GetUserFeaturesResponse } from '../proto/generated/zitadel/feature/v2beta/user_pb'; -import { GetSystemFeaturesRequest, GetSystemFeaturesResponse } from '../proto/generated/zitadel/feature/v2beta/system_pb'; -import { - GetInstanceFeaturesRequest, - GetInstanceFeaturesResponse, - ResetInstanceFeaturesRequest, - SetInstanceFeaturesRequest, - SetInstanceFeaturesResponse, -} from '../proto/generated/zitadel/feature/v2/instance_pb'; - -@Injectable({ - providedIn: 'root', -}) -export class FeatureService { - constructor(private readonly grpcService: GrpcService) {} - - public getInstanceFeatures(inheritance: boolean): Promise { - const req = new GetInstanceFeaturesRequest(); - req.setInheritance(inheritance); - return this.grpcService.feature.getInstanceFeatures(req, null).then((resp) => resp); - } - - public setInstanceFeatures(req: SetInstanceFeaturesRequest): Promise { - return this.grpcService.feature.setInstanceFeatures(req, null); - } - - public resetInstanceFeatures(): Promise { - const req = new ResetInstanceFeaturesRequest(); - return this.grpcService.feature.resetInstanceFeatures(req, null); - } - - public getOrganizationFeatures(orgId: string, inheritance: boolean): Promise { - const req = new GetOrganizationFeaturesRequest(); - req.setOrganizationId(orgId); - req.setInheritance(inheritance); - return this.grpcService.feature.getOrganizationFeatures(req, null).then((resp) => resp); - } - - public getSystemFeatures(): Promise { - const req = new GetSystemFeaturesRequest(); - return this.grpcService.feature.getSystemFeatures(req, null).then((resp) => resp); - } - - public getUserFeatures(userId: string, inheritance: boolean): Promise { - const req = new GetUserFeaturesRequest(); - req.setInheritance(inheritance); - req.setUserId(userId); - return this.grpcService.feature.getUserFeatures(req, null).then((resp) => resp); - } -} diff --git a/console/src/app/services/grpc-auth.service.ts b/console/src/app/services/grpc-auth.service.ts index 3967f1df06..198d048b6a 100644 --- a/console/src/app/services/grpc-auth.service.ts +++ b/console/src/app/services/grpc-auth.service.ts @@ -1,7 +1,18 @@ import { Injectable } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; import { OAuthService } from 'angular-oauth2-oidc'; -import { BehaviorSubject, combineLatestWith, EMPTY, mergeWith, NEVER, Observable, of, shareReplay, Subject } from 'rxjs'; +import { + BehaviorSubject, + combineLatestWith, + EMPTY, + identity, + mergeWith, + NEVER, + Observable, + of, + shareReplay, + Subject, +} from 'rxjs'; import { catchError, distinctUntilChanged, filter, finalize, map, startWith, switchMap, tap, timeout } from 'rxjs/operators'; import { @@ -326,7 +337,7 @@ export class GrpcAuthService { return new RegExp(reqRegexp).test(role); }); - const allCheck = requestedRoles.map(test).every((x) => !!x); + const allCheck = requestedRoles.map(test).every(identity); const oneCheck = requestedRoles.some(test); return requiresAll ? allCheck : oneCheck; diff --git a/console/src/app/services/grpc.service.ts b/console/src/app/services/grpc.service.ts index 52332eae15..d2add12f41 100644 --- a/console/src/app/services/grpc.service.ts +++ b/console/src/app/services/grpc.service.ts @@ -15,14 +15,20 @@ import { AuthInterceptor, AuthInterceptorProvider, NewConnectWebAuthInterceptor import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.interceptor'; import { I18nInterceptor } from './interceptors/i18n.interceptor'; import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } from './interceptors/org.interceptor'; -import { StorageService } from './storage.service'; import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb'; //@ts-ignore -import { createUserServiceClient } from '@zitadel/client/v2'; +import { createFeatureServiceClient, createUserServiceClient, createSessionServiceClient } from '@zitadel/client/v2'; //@ts-ignore import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1'; import { createGrpcWebTransport } from '@connectrpc/connect-web'; -import { FeatureServiceClient } from '../proto/generated/zitadel/feature/v2/Feature_serviceServiceClientPb'; +// @ts-ignore +import { createClientFor } from '@zitadel/client'; + +import { WebKeyService } from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb'; +import { ActionService } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; + +const createWebKeyServiceClient = createClientFor(WebKeyService); +const createActionServiceClient = createClientFor(ActionService); @Injectable({ providedIn: 'root', @@ -31,17 +37,19 @@ export class GrpcService { public auth!: AuthServiceClient; public mgmt!: ManagementServiceClient; public admin!: AdminServiceClient; - public feature!: FeatureServiceClient; public user!: UserServiceClient; public userNew!: ReturnType; + public session!: ReturnType; public mgmtNew!: ReturnType; public authNew!: ReturnType; + public featureNew!: ReturnType; + public actionNew!: ReturnType; + public webKey!: ReturnType; constructor( private readonly envService: EnvironmentService, private readonly platformLocation: PlatformLocation, private readonly authenticationService: AuthenticationService, - private readonly storageService: StorageService, private readonly translate: TranslateService, private readonly exhaustedService: ExhaustedService, private readonly authInterceptor: AuthInterceptor, @@ -87,12 +95,6 @@ export class GrpcService { // @ts-ignore interceptors, ); - this.feature = new FeatureServiceClient( - env.api, - null, - // @ts-ignore - interceptors, - ); this.user = new UserServiceClient( env.api, null, @@ -112,8 +114,12 @@ export class GrpcService { ], }); this.userNew = createUserServiceClient(transport); + this.session = createSessionServiceClient(transport); this.mgmtNew = createManagementServiceClient(transportOldAPIs); this.authNew = createAuthServiceClient(transport); + this.featureNew = createFeatureServiceClient(transport); + this.actionNew = createActionServiceClient(transport); + this.webKey = createWebKeyServiceClient(transport); const authConfig: AuthConfig = { scope: 'openid profile email', diff --git a/console/src/app/services/interceptors/auth.interceptor.ts b/console/src/app/services/interceptors/auth.interceptor.ts index 72c1670bce..65b7cbc4bf 100644 --- a/console/src/app/services/interceptors/auth.interceptor.ts +++ b/console/src/app/services/interceptors/auth.interceptor.ts @@ -17,15 +17,15 @@ const accessTokenStorageKey = 'access_token'; @Injectable({ providedIn: 'root' }) export class AuthInterceptorProvider { - public triggerDialog: Subject = new Subject(); + private readonly triggerDialog: Subject = new Subject(); constructor( - private authenticationService: AuthenticationService, - private storageService: StorageService, - private dialog: MatDialog, - private destroyRef: DestroyRef, + private readonly authenticationService: AuthenticationService, + private readonly storageService: StorageService, + private readonly dialog: MatDialog, + destroyRef: DestroyRef, ) { - this.triggerDialog.pipe(debounceTime(1000), takeUntilDestroyed(this.destroyRef)).subscribe(() => this.openDialog()); + this.triggerDialog.pipe(debounceTime(1000), takeUntilDestroyed(destroyRef)).subscribe(() => this.openDialog()); } getToken(): Observable { diff --git a/console/src/app/services/new-auth.service.ts b/console/src/app/services/new-auth.service.ts index 9a3e2365cd..4827c0db31 100644 --- a/console/src/app/services/new-auth.service.ts +++ b/console/src/app/services/new-auth.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { GrpcService } from './grpc.service'; import { AddMyAuthFactorOTPSMSResponse, + GetMyPasswordComplexityPolicyResponse, GetMyUserResponse, ListMyMetadataResponse, VerifyMyPhoneResponse, @@ -28,4 +29,8 @@ export class NewAuthService { public listMyMetadata(): Promise { return this.grpcService.authNew.listMyMetadata({}); } + + public getMyPasswordComplexityPolicy(): Promise { + return this.grpcService.authNew.getMyPasswordComplexityPolicy({}); + } } diff --git a/console/src/app/services/new-feature.service.ts b/console/src/app/services/new-feature.service.ts new file mode 100644 index 0000000000..5bb4fe8dd7 --- /dev/null +++ b/console/src/app/services/new-feature.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { GrpcService } from './grpc.service'; +import { + GetInstanceFeaturesResponse, + ResetInstanceFeaturesResponse, + SetInstanceFeaturesRequestSchema, + SetInstanceFeaturesResponse, +} from '@zitadel/proto/zitadel/feature/v2/instance_pb'; +import { MessageInitShape } from '@bufbuild/protobuf'; + +@Injectable({ + providedIn: 'root', +}) +export class NewFeatureService { + constructor(private readonly grpcService: GrpcService) {} + + public getInstanceFeatures(): Promise { + return this.grpcService.featureNew.getInstanceFeatures({}); + } + + public setInstanceFeatures( + req: MessageInitShape, + ): Promise { + return this.grpcService.featureNew.setInstanceFeatures(req); + } + + public resetInstanceFeatures(): Promise { + return this.grpcService.featureNew.resetInstanceFeatures({}); + } +} diff --git a/console/src/app/services/new-mgmt.service.ts b/console/src/app/services/new-mgmt.service.ts index 21019f8c73..6798d25f41 100644 --- a/console/src/app/services/new-mgmt.service.ts +++ b/console/src/app/services/new-mgmt.service.ts @@ -3,8 +3,10 @@ import { GrpcService } from './grpc.service'; import { GenerateMachineSecretRequestSchema, GenerateMachineSecretResponse, + GetDefaultPasswordComplexityPolicyResponse, GetLoginPolicyRequestSchema, GetLoginPolicyResponse, + GetPasswordComplexityPolicyResponse, ListUserMetadataRequestSchema, ListUserMetadataResponse, RemoveMachineSecretRequestSchema, @@ -89,4 +91,12 @@ export class NewMgmtService { ): Promise { return this.grpcService.mgmtNew.removeUserMetadata(create(RemoveUserMetadataRequestSchema, req)); } + + public getPasswordComplexityPolicy(): Promise { + return this.grpcService.mgmtNew.getPasswordComplexityPolicy({}); + } + + public getDefaultPasswordComplexityPolicy(): Promise { + return this.grpcService.mgmtNew.getDefaultPasswordComplexityPolicy({}); + } } diff --git a/console/src/app/services/password-complexity-validator-factory.service.ts b/console/src/app/services/password-complexity-validator-factory.service.ts new file mode 100644 index 0000000000..82c6d5eb58 --- /dev/null +++ b/console/src/app/services/password-complexity-validator-factory.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { ValidatorFn } from '@angular/forms'; +import { + containsLowerCaseValidator, + containsNumberValidator, + containsSymbolValidator, + containsUpperCaseValidator, + minLengthValidator, + requiredValidator, +} from '../modules/form-field/validators/validators'; +import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb'; + +@Injectable({ + providedIn: 'root', +}) +export class PasswordComplexityValidatorFactoryService { + constructor() {} + + buildValidators(policy?: PasswordComplexityPolicy) { + const validators: ValidatorFn[] = [requiredValidator]; + if (policy?.minLength) { + validators.push(minLengthValidator(Number(policy.minLength))); + } + if (policy?.hasLowercase) { + validators.push(containsLowerCaseValidator); + } + if (policy?.hasUppercase) { + validators.push(containsUpperCaseValidator); + } + if (policy?.hasNumber) { + validators.push(containsNumberValidator); + } + if (policy?.hasSymbol) { + validators.push(containsSymbolValidator); + } + return validators; + } +} diff --git a/console/src/app/services/posthog.service.ts b/console/src/app/services/posthog.service.ts index 2f9630282a..17855d7eb5 100644 --- a/console/src/app/services/posthog.service.ts +++ b/console/src/app/services/posthog.service.ts @@ -26,8 +26,16 @@ export class PosthogService implements OnDestroy { maskAllInputs: true, maskTextSelector: '*', }, + disable_session_recording: true, enable_heatmaps: true, persistence: 'memory', + loaded: (posthog) => { + posthog.onFeatureFlags((flags) => { + if (posthog.isFeatureEnabled('session_recording')) { + posthog.startSessionRecording(); + } + }); + }, }); } } diff --git a/console/src/app/services/session.service.ts b/console/src/app/services/session.service.ts new file mode 100644 index 0000000000..12e07049b4 --- /dev/null +++ b/console/src/app/services/session.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; +import { GrpcService } from './grpc.service'; +import type { MessageInitShape } from '@bufbuild/protobuf'; +import { ListSessionsRequestSchema, ListSessionsResponse } from '@zitadel/proto/zitadel/session/v2/session_service_pb'; + +@Injectable({ + providedIn: 'root', +}) +export class SessionService { + constructor(private readonly grpcService: GrpcService) {} + + public listSessions(req: MessageInitShape): Promise { + return this.grpcService.session.listSessions(req); + } +} diff --git a/console/src/app/services/user.service.ts b/console/src/app/services/user.service.ts index d0388571d3..a5bbd0aaff 100644 --- a/console/src/app/services/user.service.ts +++ b/console/src/app/services/user.service.ts @@ -1,4 +1,4 @@ -import { DestroyRef, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { GrpcService } from './grpc.service'; import { AddHumanUserRequestSchema, @@ -70,57 +70,65 @@ import { ObjectDetails } from '../proto/generated/zitadel/object_pb'; import { Timestamp } from '../proto/generated/google/protobuf/timestamp_pb'; import { HumanPhone, HumanPhoneSchema } from '@zitadel/proto/zitadel/user/v2/phone_pb'; import { OAuthService } from 'angular-oauth2-oidc'; -import { firstValueFrom, Observable, shareReplay } from 'rxjs'; -import { filter, map, startWith, tap, timeout } from 'rxjs/operators'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { debounceTime, EMPTY, Observable, of, ReplaySubject, shareReplay, switchAll, switchMap } from 'rxjs'; +import { catchError, filter, map, startWith } from 'rxjs/operators'; @Injectable({ providedIn: 'root', }) export class UserService { - private readonly userId$: Observable; - private user: UserV2 | undefined; + private user$$ = new ReplaySubject>(1); + public user$ = this.user$$.pipe( + startWith(this.getUser()), + // makes sure if many subscribers reset the observable only one wins + debounceTime(10), + switchAll(), + catchError((err) => { + // reset user observable on error + this.user$$.next(this.getUser()); + throw err; + }), + ); constructor( private readonly grpcService: GrpcService, private readonly oauthService: OAuthService, - destroyRef: DestroyRef, - ) { - this.userId$ = this.getUserId().pipe(shareReplay({ refCount: true, bufferSize: 1 })); - - // this preloads the userId and deletes the cache everytime the userId changes - this.userId$.pipe(takeUntilDestroyed(destroyRef)).subscribe(async () => { - this.user = undefined; - try { - await this.getMyUser(); - } catch (error) { - console.warn(error); - } - }); - } + ) {} private getUserId() { return this.oauthService.events.pipe( filter((event) => event.type === 'token_received'), - startWith(this.oauthService.getIdToken), map(() => this.oauthService.getIdToken()), + startWith(this.oauthService.getIdToken()), filter(Boolean), - // split jwt and get base64 encoded payload - map((token) => token.split('.')[1]), - // decode payload - map(atob), - // parse payload - map((payload) => JSON.parse(payload)), - map((payload: unknown) => { - // check if sub is in payload and is a string - if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') { - return payload.sub; + switchMap((token) => { + // we do this in a try catch so the observable will retry this logic if it fails + try { + // split jwt and get base64 encoded payload + const unparsedPayload = atob(token.split('.')[1]); + // parse payload + const payload: unknown = JSON.parse(unparsedPayload); + // check if sub is in payload and is a string + if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') { + return of(payload.sub); + } + return EMPTY; + } catch { + return EMPTY; } - throw new Error('Invalid payload'); }), ); } + private getUser() { + return this.getUserId().pipe( + switchMap((id) => this.getUserById(id)), + map((resp) => resp.user), + filter(Boolean), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + } + public addHumanUser(req: MessageInitShape): Promise { return this.grpcService.userNew.addHumanUser(create(AddHumanUserRequestSchema, req)); } @@ -129,20 +137,6 @@ export class UserService { return this.grpcService.userNew.listUsers(req); } - public async getMyUser(): Promise { - const userId = await firstValueFrom(this.userId$.pipe(timeout(2000))); - if (this.user) { - return this.user; - } - const resp = await this.getUserById(userId); - if (!resp.user) { - throw new Error("Couldn't find user"); - } - - this.user = resp.user; - return resp.user; - } - public getUserById(userId: string): Promise { return this.grpcService.userNew.getUserByID({ userId }); } diff --git a/console/src/app/services/webkeys.service.ts b/console/src/app/services/webkeys.service.ts new file mode 100644 index 0000000000..9a26be4712 --- /dev/null +++ b/console/src/app/services/webkeys.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { GrpcService } from './grpc.service'; +import type { MessageInitShape } from '@bufbuild/protobuf'; +import { + DeleteWebKeyResponse, + ListWebKeysResponse, + CreateWebKeyRequestSchema, + CreateWebKeyResponse, + ActivateWebKeyResponse, +} from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb'; + +@Injectable({ + providedIn: 'root', +}) +export class WebKeysService { + constructor(private readonly grpcService: GrpcService) {} + + public ListWebKeys(): Promise { + return this.grpcService.webKey.listWebKeys({}); + } + + public DeleteWebKey(id: string): Promise { + return this.grpcService.webKey.deleteWebKey({ id }); + } + + public CreateWebKey(req: MessageInitShape): Promise { + return this.grpcService.webKey.createWebKey(req); + } + + public ActivateWebKey(id: string): Promise { + return this.grpcService.webKey.activateWebKey({ id }); + } +} diff --git a/console/src/app/utils/language.ts b/console/src/app/utils/language.ts index 4ef63dcb28..22eac99b3c 100644 --- a/console/src/app/utils/language.ts +++ b/console/src/app/utils/language.ts @@ -17,6 +17,7 @@ export const supportedLanguages = [ 'sv', 'hu', 'ko', + 'ro', ]; -export const supportedLanguagesRegexp: RegExp = /de|en|es|fr|id|it|ja|pl|zh|bg|pt|mk|cs|ru|nl|sv|hu|ko/; +export const supportedLanguagesRegexp: RegExp = /de|en|es|fr|id|it|ja|pl|zh|bg|pt|mk|cs|ru|nl|sv|hu|ko|ro/; export const fallbackLanguage: string = 'en'; diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 78b94614d6..b98204a917 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Потоци", "DESCRIPTION": "Изберете поток за удостоверяване и активирайте вашето действие при конкретно събитие в този поток." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, нова и подобрена версия на Actions, вече е налична. Настоящата версия все още е достъпна, но бъдещото развитие ще бъде фокусирано върху новата, която в крайна сметка ще замени текущата версия." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "Животът на неактивния refresh токен е максималното време, през което refresh токен може да не се използва." } }, + "WEB_KEYS": { + "DESCRIPTION": "Управлявайте вашите OIDC уеб ключове, за да подписвате и валидирате токени сигурно за вашата ZITADEL инстанция.", + "TABLE": { + "TITLE": "Активни и бъдещи уеб ключове", + "DESCRIPTION": "Вашите активни и предстоящи уеб ключове. Активирането на нов ключ ще деактивира текущия.", + "NOTE": "Забележка: Крайна точка JWKs OIDC връща кешируем отговор (по подразбиране 5 минути). Избягвайте активирането на ключ твърде рано, тъй като той може да не е наличен в кеша и клиентите.", + "ACTIVATE": "Активирайте следващия уеб ключ", + "ACTIVE": "В момента активен", + "NEXT": "Следващ в опашката", + "FUTURE": "Бъдещ", + "WARNING": "Уеб ключът е на по-малко от 5 минути" + }, + "CREATE": { + "TITLE": "Създаване на нов уеб ключ", + "DESCRIPTION": "Създаването на нов уеб ключ го добавя към вашия списък. ZITADEL използва ключове RSA2048 с хеш SHA256 по подразбиране.", + "KEY_TYPE": "Тип ключ", + "BITS": "Битове", + "HASHER": "Хешер", + "CURVE": "Крива" + }, + "PREVIOUS_TABLE": { + "TITLE": "Предишни уеб ключове", + "DESCRIPTION": "Това са вашите предишни уеб ключове, които вече не са активни.", + "DEACTIVATED_ON": "Деактивиран на" + } + }, "MESSAGE_TEXTS": { "TITLE": "Текстове на съобщенията", "DESCRIPTION": "Персонализирайте текстовете на вашите имейл или SMS уведомления. Ако искате да деактивирате някои от езиците, ограничете ги в настройките за език на вашите инстанции.", @@ -501,6 +528,118 @@ "DOWNLOAD": "Изтегляне", "APPLY": "Прилагам" }, + "ACTIONSTWO": { + "BETA_NOTE": "В момента използвате новата версия Actions V2, която е в бета фаза. Предишната версия 1 все още е достъпна, но ще бъде спряна в бъдеще. Моля, съобщавайте за всякакви проблеми или изпратете обратна връзка.", + "EXECUTION": { + "TITLE": "Действия", + "DESCRIPTION": "Действията ви позволяват да изпълнявате персонализиран код в отговор на API заявки, събития или специфични функции. Използвайте ги, за да разширите Zitadel, да автоматизирате работни процеси и да се интегрирате с други системи.", + "TYPES": { + "request": "Заявка", + "response": "Отговор", + "event": "Събития", + "function": "Функция" + }, + "DIALOG": { + "CREATE_TITLE": "Създаване на действие", + "UPDATE_TITLE": "Актуализиране на действие", + "TYPE": { + "DESCRIPTION": "Изберете кога искате да се изпълни това действие", + "REQUEST": { + "TITLE": "Заявка", + "DESCRIPTION": "Заявки, които се появяват в Zitadel. Това може да бъде нещо като заявка за вход." + }, + "RESPONSE": { + "TITLE": "Отговор", + "DESCRIPTION": "Отговор от заявка в Zitadel. Помислете за отговора, който получавате при извличане на потребител." + }, + "EVENTS": { + "TITLE": "Събития", + "DESCRIPTION": "Събития, които се случват в Zitadel. Това може да бъде нещо като създаване на потребителски акаунт, успешно влизане и т.н." + }, + "FUNCTIONS": { + "TITLE": "Функции", + "DESCRIPTION": "Функции, които можете да извикате в Zitadel. Това може да бъде всичко от изпращане на имейл до създаване на потребител." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Изберете дали това действие се прилага към всички заявки, конкретна услуга (напр. управление на потребители) или единична заявка (напр. създаване на потребител).", + "ALL": { + "TITLE": "Всички", + "DESCRIPTION": "Изберете това, ако искате да изпълните действието си при всяка заявка" + }, + "ALL_EVENTS": "Изберете това, ако искате действието да се изпълнява при всяко събитие", + "SELECT_SERVICE": { + "TITLE": "Избор на услуга", + "DESCRIPTION": "Изберете услуга на Zitadel за вашето действие." + }, + "SELECT_METHOD": { + "TITLE": "Избор на метод", + "DESCRIPTION": "Ако искате да изпълните само при конкретна заявка, изберете я тук", + "NOTE": "Ако не изберете метод, действието ви ще се изпълни при всяка заявка във вашата избрана услуга." + }, + "FUNCTIONNAME": { + "TITLE": "Име на функция", + "DESCRIPTION": "Изберете функцията, която искате да изпълните" + }, + "SELECT_GROUP": { + "TITLE": "Задаване на група", + "DESCRIPTION": "Ако искате да изпълните само върху група събития, задайте групата тук" + }, + "SELECT_EVENT": { + "TITLE": "Избор на събитие", + "DESCRIPTION": "Ако искате да изпълните само при конкретно събитие, посочете го тук" + } + }, + "TARGET": { + "DESCRIPTION": "Можете да изберете да изпълните цел или да я изпълните при същите условия като други цели.", + "TARGET": { + "DESCRIPTION": "Целта, която искате да изпълните за това действие" + }, + "CONDITIONS": { + "DESCRIPTION": "Условия за изпълнение" + } + } + }, + "TABLE": { + "CONDITION": "Условие", + "TYPE": "Тип", + "TARGET": "Цел", + "CREATIONDATE": "Дата на създаване" + } + }, + "TARGET": { + "TITLE": "Цели", + "DESCRIPTION": "Целта е дестинацията на кода, който искате да изпълните от действие. Създайте цел тук и я добавете към вашите действия.", + "CREATE": { + "TITLE": "Създаване на вашата цел", + "DESCRIPTION": "Създайте своя собствена цел извън Zitadel", + "NAME": "Име", + "NAME_DESCRIPTION": "Дайте на целта си ясно, описателно име, за да я идентифицирате лесно по-късно", + "TYPE": "Тип", + "TYPES": { + "restWebhook": "REST уеб кука", + "restCall": "REST извикване", + "restAsync": "REST асинхронно" + }, + "TYPES_DESCRIPTION": "Webhook, обаждането обработва кода на състоянието, но отговорът е без значение\nCall, обаждането обработва кода на състоянието и отговора\nAsync, обаждането не обработва нито кода на състоянието, нито отговора, но може да бъде извикано паралелно с други цели", + "ENDPOINT": "Крайна точка", + "ENDPOINT_DESCRIPTION": "Въведете крайната точка, където се хоства вашият код. Уверете се, че е достъпна за нас!", + "TIMEOUT": "Време за изчакване", + "TIMEOUT_DESCRIPTION": "Задайте максималното време, за което вашата цел трябва да отговори. Ако отнеме повече време, ще спрем заявката.", + "INTERRUPT_ON_ERROR": "Прекъсване при грешка", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Спрете всички изпълнения, когато целите върнат грешка", + "INTERRUPT_ON_ERROR_WARNING": "Внимание: „Прекъсване при грешка“ спира операциите при неуспех, което може да доведе до блокиране. Тествайте с изключена опция, за да предотвратите блокиране на входа/създаването.", + "AWAIT_RESPONSE": "Изчакване на отговор", + "AWAIT_RESPONSE_DESCRIPTION": "Ще изчакаме отговор, преди да направим нещо друго. Полезно, ако възнамерявате да използвате множество цели за едно действие" + }, + "TABLE": { + "NAME": "Име", + "ENDPOINT": "Крайна точка", + "CREATIONDATE": "Дата на създаване", + "REORDER": "Преоразмеряване" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Има контрол върху цялата инстанция, включително всички организации", "IAM_OWNER_VIEWER": "Има разрешение да прегледа целия екземпляр, включително всички организации", @@ -789,7 +928,10 @@ "PHONESECTION": "Телефонни номера", "PASSWORDSECTION": "Първоначална парола", "ADDRESSANDPHONESECTION": "Телефонен номер", - "INITMAILDESCRIPTION": "Ако са избрани и двете опции, няма да бъде изпратен имейл за инициализация. " + "INITMAILDESCRIPTION": "Ако са избрани и двете опции, няма да бъде изпратен имейл за инициализация. ", + "SETUPAUTHENTICATIONLATER": "Настройте удостоверяване по-късно за този потребител.", + "INVITATION": "Изпратете покана по имейл за настройка на удостоверяване и потвърждение на имейл.", + "INITIALPASSWORD": "Задайте начална парола за потребителя." }, "CODEDIALOG": { "TITLE": "Потвърдете телефонния номер", @@ -1353,6 +1495,7 @@ "BRANDING": "Брандиране", "PRIVACYPOLICY": "Политика за бедност", "OIDC": "Живот и изтичане на OIDC Token", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Тайна поява", "SECURITY": "Настройки на сигурността", "EVENTS": "Събития", @@ -1368,7 +1511,8 @@ "APPEARANCE": "Външен вид", "OTHER": "други", "STORAGE": "Съхранение" - } + }, + "BETA": "БЕТА" }, "SETTING": { "LANGUAGES": { @@ -1398,7 +1542,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1489,6 +1634,16 @@ "ACTIONS_DESCRIPTION": "Действия v2 позволяват управление на выполнения на данни и цели. Ако флагът е активиран, ще можете да използвате новия API и неговите функции.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Завършване на сесия", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Ако флагът е активиран, ще можете да прекратите единична сесия от UI за вход, като предоставите id_token с `sid` претенция като id_token_hint на крайната точка на end_session. Имайте предвид, че в момента всички сесии от същия потребителски агент (браузър) се прекратяват в UI за вход. Сесиите, управлявани чрез API на сесията, вече позволяват прекратяването на единични сесии.", + "DEBUGOIDCPARENTERROR": "Отстраняване на грешки на OIDC родителя", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Ако флагът е активиран, грешката на OIDC родителя ще бъде записана в конзолата.", + "DISABLEUSERTOKENEVENT": "Деактивиране на събитие за потребителски токен", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Активиране на Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout имплементира OpenID Connect Back-Channel Logout 1.0 и може да се използва за уведомяване на клиентите за прекратяване на сесията при OpenID доставчика.", + "PERMISSIONCHECKV2": "Проверка на разрешения V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Ако флагът е активиран, ще можете да използвате новия API и неговите функции.", + "WEBKEY": "Уеб ключ", + "WEBKEY_DESCRIPTION": "Ако флагът е активиран, ще можете да използвате новия API и неговите функции.", "STATES": { "INHERITED": "Наследено", "ENABLED": "Активирано", @@ -1501,7 +1656,10 @@ }, "RESET": "Задай всички на наследено", "CONSOLEUSEV2USERAPI": "Използвайте V2 API в конзолата за създаване на потребител", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Когато този флаг е активиран, конзолата използва V2 User API за създаване на нови потребители. С V2 API новосъздадените потребители започват без начален статус." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Когато този флаг е активиран, конзолата използва V2 User API за създаване на нови потребители. С V2 API новосъздадените потребители започват без начален статус.", + "LOGINV2": "Вход V2", + "LOGINV2_DESCRIPTION": "Активирането на това включва новия потребителски интерфейс за вход, базиран на TypeScript, с подобрена сигурност, производителност и възможности за персонализиране.", + "LOGINV2_BASEURI": "Базов URI" }, "DIALOG": { "RESET": { @@ -1638,7 +1796,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Проверката на имейл е извършена", @@ -2584,7 +2744,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Добавяне на мениджър", diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index df96c30c2d..390c5dcdbd 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flows", "DESCRIPTION": "Vyberte proces autentizace a spusťte vaši akci na konkrétní události v rámci tohoto procesu." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, nová a vylepšená verze Actions, je nyní k dispozici. Aktuální verze je stále přístupná, ale budoucí vývoj se zaměří na novou verzi, která nakonec nahradí tu současnou." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "Životnost nečinného refresh tokenu je maximální doba, po kterou může být refresh token nepoužitý." } }, + "WEB_KEYS": { + "DESCRIPTION": "Spravujte své OIDC webové klíče pro bezpečné podepisování a ověřování tokenů ve vaší instanci ZITADEL.", + "TABLE": { + "TITLE": "Aktivní a budoucí webové klíče", + "DESCRIPTION": "Vaše aktivní a nadcházející webové klíče. Aktivací nového klíče dojde k deaktivaci aktuálního.", + "NOTE": "Poznámka: Koncový bod JWKs OIDC vrací odpověď uložitelnou do mezipaměti (výchozí 5 minut). Vyhněte se příliš brzké aktivaci klíče, protože nemusí být dostupný v mezipaměti a klientům.", + "ACTIVATE": "Aktivovat další webový klíč", + "ACTIVE": "Aktuálně aktivní", + "NEXT": "Další v řadě", + "FUTURE": "Budoucí", + "WARNING": "Webový klíč je starý méně než 5 minut" + }, + "CREATE": { + "TITLE": "Vytvořit nový webový klíč", + "DESCRIPTION": "Vytvořením nového webového klíče jej přidáte do svého seznamu. ZITADEL používá klíče RSA2048 s hashováním SHA256 jako výchozí.", + "KEY_TYPE": "Typ klíče", + "BITS": "Bity", + "HASHER": "Hasher", + "CURVE": "Křivka" + }, + "PREVIOUS_TABLE": { + "TITLE": "Předchozí webové klíče", + "DESCRIPTION": "Toto jsou vaše předchozí webové klíče, které již nejsou aktivní.", + "DEACTIVATED_ON": "Deaktivováno dne" + } + }, "MESSAGE_TEXTS": { "TITLE": "Texty zpráv", "DESCRIPTION": "Přizpůsob si texty svých e-mailových nebo SMS notifikací. Pokud chceš některé z jazyků zakázat, omez je ve svém nastavení jazyků instance.", @@ -502,6 +529,118 @@ "DOWNLOAD": "Stáhnout", "APPLY": "Platit" }, + "ACTIONSTWO": { + "BETA_NOTE": "Aktuálně používáte novou verzi Actions V2, která je v beta verzi. Předchozí verze 1 je stále k dispozici, ale v budoucnu bude ukončena. Prosím, hlaste jakékoliv problémy nebo zpětnou vazbu.", + "EXECUTION": { + "TITLE": "Akce", + "DESCRIPTION": "Akce vám umožňují spouštět vlastní kód v reakci na požadavky API, události nebo specifické funkce. Použijte je k rozšíření Zitadel, automatizaci pracovních postupů a integraci s dalšími systémy.", + "TYPES": { + "request": "Požadavek", + "response": "Odpověď", + "event": "Události", + "function": "Funkce" + }, + "DIALOG": { + "CREATE_TITLE": "Vytvořit akci", + "UPDATE_TITLE": "Aktualizovat akci", + "TYPE": { + "DESCRIPTION": "Vyberte, kdy chcete tuto akci spustit", + "REQUEST": { + "TITLE": "Požadavek", + "DESCRIPTION": "Požadavky, které se vyskytují v rámci Zitadel. Může to být něco jako volání požadavku na přihlášení." + }, + "RESPONSE": { + "TITLE": "Odpověď", + "DESCRIPTION": "Odpověď na požadavek v rámci Zitadel. Představte si odpověď, kterou získáte při načítání uživatele." + }, + "EVENTS": { + "TITLE": "Události", + "DESCRIPTION": "Události, které se dějí v rámci Zitadel. Může to být cokoli, jako je vytvoření uživatelského účtu, úspěšné přihlášení atd." + }, + "FUNCTIONS": { + "TITLE": "Funkce", + "DESCRIPTION": "Funkce, které můžete volat v rámci Zitadel. Může to být cokoli od odeslání e-mailu po vytvoření uživatele." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Vyberte, zda se tato akce vztahuje na všechny požadavky, konkrétní službu (např. správa uživatelů) nebo jeden požadavek (např. vytvoření uživatele).", + "ALL": { + "TITLE": "Všechny", + "DESCRIPTION": "Vyberte tuto možnost, pokud chcete spustit akci pro každý požadavek" + }, + "ALL_EVENTS": "Vyberte toto, pokud chcete spustit akci při každé události", + "SELECT_SERVICE": { + "TITLE": "Vybrat službu", + "DESCRIPTION": "Vyberte službu Zitadel pro svou akci." + }, + "SELECT_METHOD": { + "TITLE": "Vybrat metodu", + "DESCRIPTION": "Pokud chcete spustit pouze pro konkrétní požadavek, vyberte jej zde", + "NOTE": "Pokud nevyberete metodu, vaše akce se spustí pro každý požadavek ve vaší vybrané službě." + }, + "FUNCTIONNAME": { + "TITLE": "Název funkce", + "DESCRIPTION": "Vyberte funkci, kterou chcete spustit" + }, + "SELECT_GROUP": { + "TITLE": "Nastavit skupinu", + "DESCRIPTION": "Pokud chcete spustit pouze pro skupinu událostí, nastavte zde skupinu" + }, + "SELECT_EVENT": { + "TITLE": "Vybrat událost", + "DESCRIPTION": "Pokud chcete spustit pouze pro konkrétní událost, zadejte ji zde" + } + }, + "TARGET": { + "DESCRIPTION": "Můžete se rozhodnout spustit cíl nebo jej spustit za stejných podmínek jako jiné cíle.", + "TARGET": { + "DESCRIPTION": "Cíl, který chcete spustit pro tuto akci" + }, + "CONDITIONS": { + "DESCRIPTION": "Podmínky spuštění" + } + } + }, + "TABLE": { + "CONDITION": "Podmínka", + "TYPE": "Typ", + "TARGET": "Cíl", + "CREATIONDATE": "Datum vytvoření" + } + }, + "TARGET": { + "TITLE": "Cíle", + "DESCRIPTION": "Cíl je cíl kódu, který chcete spustit z akce. Vytvořte zde cíl a přidejte jej do svých akcí.", + "CREATE": { + "TITLE": "Vytvořit cíl", + "DESCRIPTION": "Vytvořte si vlastní cíl mimo Zitadel", + "NAME": "Název", + "NAME_DESCRIPTION": "Dejte svému cíli jasný, popisný název, aby bylo snadné jej později identifikovat", + "TYPE": "Typ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Volání", + "restAsync": "REST Asynchronní" + }, + "TYPES_DESCRIPTION": "Webhook, volání zpracovává stavový kód, ale odpověď je irelevantní\nCall, volání zpracovává stavový kód a odpověď\nAsync, volání nezpracovává ani stavový kód, ani odpověď, ale může být spuštěno paralelně s jinými cíli", + "ENDPOINT": "Koncový bod", + "ENDPOINT_DESCRIPTION": "Zadejte koncový bod, kde je hostován váš kód. Ujistěte se, že je pro nás přístupný!", + "TIMEOUT": "Časový limit", + "TIMEOUT_DESCRIPTION": "Nastavte maximální dobu, po kterou musí váš cíl odpovědět. Pokud to trvá déle, požadavek zastavíme.", + "INTERRUPT_ON_ERROR": "Přerušit při chybě", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Zastavte všechna spuštění, když cíle vrátí chybu", + "INTERRUPT_ON_ERROR_WARNING": "Pozor: „Přerušit při chybě“ zastaví operace při selhání, což může vést k zablokování. Otestujte s vypnutou možností, abyste předešli zablokování přihlášení/vytváření.", + "AWAIT_RESPONSE": "Čekat na odpověď", + "AWAIT_RESPONSE_DESCRIPTION": "Před provedením čehokoli jiného počkáme na odpověď. Užitečné, pokud hodláte použít více cílů pro jednu akci" + }, + "TABLE": { + "NAME": "Název", + "ENDPOINT": "Koncový bod", + "CREATIONDATE": "Datum vytvoření", + "REORDER": "Změnit pořadí" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Má kontrolu nad celou instancí, včetně všech organizací", "IAM_OWNER_VIEWER": "Má oprávnění prohlížet celou instanci, včetně všech organizací", @@ -790,7 +929,10 @@ "PHONESECTION": "Telefonní čísla", "PASSWORDSECTION": "Prvotní heslo", "ADDRESSANDPHONESECTION": "Telefonní číslo", - "INITMAILDESCRIPTION": "Pokud jsou vybrány obě možnosti, nebude odeslán e-mail pro inicializaci. Pokud je vybrána pouze jedna z možností, bude odeslán e-mail pro poskytnutí/ověření údajů." + "INITMAILDESCRIPTION": "Pokud jsou vybrány obě možnosti, nebude odeslán e-mail pro inicializaci. Pokud je vybrána pouze jedna z možností, bude odeslán e-mail pro poskytnutí/ověření údajů.", + "SETUPAUTHENTICATIONLATER": "Nastavte ověřování později pro tohoto uživatele.", + "INVITATION": "Odešlete pozvánkový e-mail pro nastavení ověřování a ověření e-mailu.", + "INITIALPASSWORD": "Nastavte počáteční heslo pro uživatele." }, "CODEDIALOG": { "TITLE": "Ověření telefonního čísla", @@ -1354,6 +1496,7 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Zásady ochrany osobních údajů", "OIDC": "Životnost a expirace OIDC tokenu", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Generátor tajemství", "SECURITY": "Bezpečnostní nastavení", "EVENTS": "Události", @@ -1369,7 +1512,8 @@ "APPEARANCE": "Vzhled", "OTHER": "Ostatní", "STORAGE": "Data" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { @@ -1399,7 +1543,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1490,6 +1635,16 @@ "ACTIONS_DESCRIPTION": "Akce v2 umožňují správu datových provedení a cílů. Pokud je tento příznak povolen, budete moci používat nové API a jeho funkce.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 ukončení relace", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Pokud je příznak aktivován, budete moci ukončit jedinou relaci z rozhraní pro přihlášení zadáním id_token s nárokem `sid` jako id_token_hint na koncovém bodu end_session. Poznamenejte si, že v současné době jsou v rozhraní pro přihlášení ukončeny všechny relace ze stejného uživatelského agenta (prohlížeče). Relace spravované prostřednictvím rozhraní API relace již umožňují ukončení jednotlivých relací.", + "DEBUGOIDCPARENTERROR": "Debugování chyby rodiče OIDC", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Pokud je příznak povolen, chyba rodiče OIDC bude zaznamenána v konzoli.", + "DISABLEUSERTOKENEVENT": "Zakázat událost uživatelského tokenu", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Povolit Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout implementuje OpenID Connect Back-Channel Logout 1.0 a může být použit k informování klientů o ukončení relace u poskytovatele OpenID.", + "PERMISSIONCHECKV2": "Kontrola oprávnění V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Pokud je příznak povolen, budete moci používat nový API a jeho funkce.", + "WEBKEY": "Webový klíč", + "WEBKEY_DESCRIPTION": "Pokud je příznak povolen, budete moci používat nový API a jeho funkce.", "STATES": { "INHERITED": "Děděno", "ENABLED": "Povoleno", @@ -1502,7 +1657,10 @@ }, "RESET": "Nastavit vše na děděné", "CONSOLEUSEV2USERAPI": "Použijte V2 API v konzoli pro vytvoření uživatele", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Když je tato příznak povolen, konzole používá V2 User API k vytvoření nových uživatelů. S V2 API nově vytvoření uživatelé začínají bez počátečního stavu." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Když je tato příznak povolen, konzole používá V2 User API k vytvoření nových uživatelů. S V2 API nově vytvoření uživatelé začínají bez počátečního stavu.", + "LOGINV2": "Přihlášení V2", + "LOGINV2_DESCRIPTION": "Povolením této možnosti se aktivuje nové přihlašovací rozhraní založené na TypeScriptu s vylepšeným zabezpečením, výkonem a přizpůsobitelností.", + "LOGINV2_BASEURI": "Základní URI" }, "DIALOG": { "RESET": { @@ -1639,7 +1797,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Ověření e-mailu dokončeno", @@ -2597,7 +2757,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Přidat manažera", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index e49c17470d..e73c883bd2 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flows", "DESCRIPTION": "Wähle einen Authentifizierungsflow und löse deine Aktionen bei einem spezifischen Ereignis innerhalb dieses Flows aus." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, eine neue und verbesserte Version von Actions, ist jetzt verfügbar. Die aktuelle Version ist weiterhin zugänglich, aber unsere zukünftige Entwicklung wird sich auf die neue Version konzentrieren, die schließlich die aktuelle ersetzen wird." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "Die maximale Inaktivitätsdauer eines Aktualisierungstokens ist die maximale Zeit, in der ein Aktualisierungstoken unbenutzt sein kann." } }, + "WEB_KEYS": { + "DESCRIPTION": "Verwalte deine OIDC Web Keys, um Tokens für deine ZITADEL-Instanz sicher zu signieren und zu validieren.", + "TABLE": { + "TITLE": "Aktive und zukünftige Web Keys", + "DESCRIPTION": "Deine aktiven und kommenden Web Keys. Das Aktivieren eines neuen Schlüssels deaktiviert den aktuellen.", + "NOTE": "Hinweis: Der JWKs OIDC-Endpunkt gibt eine zwischenspeicherbare Antwort zurück (Standard: 5 Min.). Vermeide es, einen Schlüssel zu früh zu aktivieren, da er möglicherweise noch nicht in Caches und Clients verfügbar ist.", + "ACTIVATE": "Nächsten Web Key aktivieren", + "ACTIVE": "Derzeit aktiv", + "NEXT": "Als Nächstes in der Warteschlange", + "FUTURE": "Zukünftig", + "WARNING": "Der Web Key ist weniger als 5 Minuten alt" + }, + "CREATE": { + "TITLE": "Neuen Web Key erstellen", + "DESCRIPTION": "Das Erstellen eines neuen Web Keys fügt ihn zu deiner Liste hinzu. ZITADEL verwendet standardmäßig RSA2048-Schlüssel mit einem SHA256-Hasher.", + "KEY_TYPE": "Schlüsseltyp", + "BITS": "Bits", + "HASHER": "Hasher", + "CURVE": "Kurve" + }, + "PREVIOUS_TABLE": { + "TITLE": "Frühere Web Keys", + "DESCRIPTION": "Dies sind deine früheren Web Keys, die nicht mehr aktiv sind.", + "DEACTIVATED_ON": "Deaktiviert am" + } + }, "MESSAGE_TEXTS": { "TITLE": "Nachrichtentexte", "DESCRIPTION": "Passe die Texte deiner Benachrichtigungs-E-Mails oder SMS-Nachrichten an. Wenn du einige der Sprachen deaktivieren möchtest, beschränke sie in den Spracheinstellungen deiner Instanz.", @@ -502,6 +529,118 @@ "DOWNLOAD": "Herunterladen", "APPLY": "Anwenden" }, + "ACTIONSTWO": { + "BETA_NOTE": "Sie verwenden derzeit die neuen Actions V2, die sich in der Beta-Phase befinden. Version 1 ist weiterhin verfügbar, wird jedoch in Zukunft eingestellt. Bitte melden Sie Probleme oder Feedback.", + "EXECUTION": { + "TITLE": "Aktionen", + "DESCRIPTION": "Aktionen ermöglichen es Ihnen, benutzerdefinierten Code als Reaktion auf API-Anfragen, Ereignisse oder bestimmte Funktionen auszuführen. Verwenden Sie sie, um Zitadel zu erweitern, Arbeitsabläufe zu automatisieren und sich in andere Systeme zu integrieren.", + "TYPES": { + "request": "Anfrage", + "response": "Antwort", + "event": "Ereignisse", + "function": "Funktion" + }, + "DIALOG": { + "CREATE_TITLE": "Eine Aktion erstellen", + "UPDATE_TITLE": "Eine Aktion aktualisieren", + "TYPE": { + "DESCRIPTION": "Wählen Sie aus, wann diese Aktion ausgeführt werden soll", + "REQUEST": { + "TITLE": "Anfrage", + "DESCRIPTION": "Anfragen, die innerhalb von Zitadel auftreten. Dies könnte so etwas wie ein Login-Anfrageaufruf sein." + }, + "RESPONSE": { + "TITLE": "Antwort", + "DESCRIPTION": "Eine Antwort auf eine Anfrage innerhalb von Zitadel. Denken Sie an die Antwort, die Sie beim Abrufen eines Benutzers erhalten." + }, + "EVENTS": { + "TITLE": "Ereignisse", + "DESCRIPTION": "Ereignisse, die innerhalb von Zitadel stattfinden. Dies könnte alles sein, wie z.B. das Erstellen eines Benutzerkontos, ein erfolgreicher Login usw." + }, + "FUNCTIONS": { + "TITLE": "Funktionen", + "DESCRIPTION": "Funktionen, die Sie innerhalb von Zitadel aufrufen können. Dies könnte alles sein, vom Senden einer E-Mail bis zum Erstellen eines Benutzers." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Wählen Sie aus, ob diese Aktion für alle Anfragen, einen bestimmten Dienst (z.B. Benutzerverwaltung) oder eine einzelne Anfrage (z.B. Benutzer erstellen) gelten soll.", + "ALL": { + "TITLE": "Alle", + "DESCRIPTION": "Wählen Sie dies aus, wenn Sie Ihre Aktion bei jeder Anfrage ausführen möchten" + }, + "ALL_EVENTS": "Wähle dies aus, wenn du deine Aktion bei jedem Ereignis ausführen möchtest", + "SELECT_SERVICE": { + "TITLE": "Dienst auswählen", + "DESCRIPTION": "Wählen Sie einen Zitadel-Dienst für Ihre Aktion aus." + }, + "SELECT_METHOD": { + "TITLE": "Methode auswählen", + "DESCRIPTION": "Wenn Sie nur bei einer bestimmten Anfrage ausführen möchten, wählen Sie sie hier aus", + "NOTE": "Wenn Sie keine Methode auswählen, wird Ihre Aktion bei jeder Anfrage in Ihrem ausgewählten Dienst ausgeführt." + }, + "FUNCTIONNAME": { + "TITLE": "Funktionsname", + "DESCRIPTION": "Wählen Sie die Funktion aus, die Sie ausführen möchten" + }, + "SELECT_GROUP": { + "TITLE": "Gruppe festlegen", + "DESCRIPTION": "Wenn Sie nur bei einer Gruppe von Ereignissen ausführen möchten, legen Sie die Gruppe hier fest" + }, + "SELECT_EVENT": { + "TITLE": "Ereignis auswählen", + "DESCRIPTION": "Wenn Sie nur bei einem bestimmten Ereignis ausführen möchten, geben Sie es hier an" + } + }, + "TARGET": { + "DESCRIPTION": "Sie können wählen, ob Sie ein Ziel ausführen oder es unter den gleichen Bedingungen wie andere Ziele ausführen möchten.", + "TARGET": { + "DESCRIPTION": "Das Ziel, das Sie für diese Aktion ausführen möchten" + }, + "CONDITIONS": { + "DESCRIPTION": "Ausführungsbedingungen" + } + } + }, + "TABLE": { + "CONDITION": "Bedingung", + "TYPE": "Typ", + "TARGET": "Ziel", + "CREATIONDATE": "Erstellungsdatum" + } + }, + "TARGET": { + "TITLE": "Ziele", + "DESCRIPTION": "Ein Ziel ist das Ziel des Codes, den Sie von einer Aktion ausführen möchten. Erstellen Sie hier ein Ziel und fügen Sie es Ihren Aktionen hinzu.", + "CREATE": { + "TITLE": "Ihr Ziel erstellen", + "DESCRIPTION": "Erstellen Sie Ihr eigenes Ziel außerhalb von Zitadel", + "NAME": "Name", + "NAME_DESCRIPTION": "Geben Sie Ihrem Ziel einen klaren, beschreibenden Namen, um es später leicht identifizieren zu können", + "TYPE": "Typ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Aufruf", + "restAsync": "REST Asynchron" + }, + "TYPES_DESCRIPTION": "Webhook, der Aufruf verarbeitet den Statuscode, aber die Antwort ist irrelevant\nCall, der Aufruf verarbeitet den Statuscode und die Antwort\nAsync, der Aufruf verarbeitet weder Statuscode noch Antwort, kann aber parallel zu anderen Zielen aufgerufen werden", + "ENDPOINT": "Endpunkt", + "ENDPOINT_DESCRIPTION": "Geben Sie den Endpunkt ein, an dem Ihr Code gehostet wird. Stellen Sie sicher, dass er für uns zugänglich ist!", + "TIMEOUT": "Timeout", + "TIMEOUT_DESCRIPTION": "Legen Sie die maximale Zeit fest, die Ihr Ziel zum Antworten hat. Wenn es länger dauert, stoppen wir die Anfrage.", + "INTERRUPT_ON_ERROR": "Bei Fehler unterbrechen", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Stoppen Sie alle Ausführungen, wenn die Ziele einen Fehler zurückgeben", + "INTERRUPT_ON_ERROR_WARNING": "Achtung: „Bei Fehler unterbrechen“ stoppt Vorgänge bei einem Fehler und kann zur Sperrung führen. Testen Sie mit deaktivierter Option, um Login/Erstellung nicht zu blockieren.", + "AWAIT_RESPONSE": "Auf Antwort warten", + "AWAIT_RESPONSE_DESCRIPTION": "Wir warten auf eine Antwort, bevor wir etwas anderes tun. Nützlich, wenn Sie mehrere Ziele für eine einzelne Aktion verwenden möchten" + }, + "TABLE": { + "NAME": "Name", + "ENDPOINT": "Endpunkt", + "CREATIONDATE": "Erstellungsdatum", + "REORDER": "Verschieben" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Hat die Kontrolle über die gesamte Instanz, einschließlich aller Organisationen", "IAM_OWNER_VIEWER": "Hat die Leseberechtigung, die gesamte Instanz einschließlich aller Organisationen zu überprüfen", @@ -790,7 +929,10 @@ "PHONESECTION": "Telefonnummer", "PASSWORDSECTION": "Setze ein initiales Passwort.", "ADDRESSANDPHONESECTION": "Telefonnummer", - "INITMAILDESCRIPTION": "Wenn beide Optionen ausgewählt sind, wird keine E-Mail zur Initialisierung gesendet. Wenn nur eine der Optionen ausgewählt ist, wird eine E-Mail zur Verifikation der Daten gesendet." + "INITMAILDESCRIPTION": "Wenn beide Optionen ausgewählt sind, wird keine E-Mail zur Initialisierung gesendet. Wenn nur eine der Optionen ausgewählt ist, wird eine E-Mail zur Verifikation der Daten gesendet.", + "SETUPAUTHENTICATIONLATER": "Authentifizierung später für diesen Benutzer einrichten.", + "INVITATION": "Eine Einladung per E-Mail für die Authentifizierungseinrichtung und E-Mail-Verifizierung senden.", + "INITIALPASSWORD": "Setze ein initiales Passwort für den Benutzer." }, "CODEDIALOG": { "TITLE": "Telefonnummer verifizieren", @@ -1354,6 +1496,7 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Datenschutzrichtlinie", "OIDC": "OIDC Token Lifetime und Expiration", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Secret Generator", "SECURITY": "Sicherheitseinstellungen", "EVENTS": "Events", @@ -1369,7 +1512,8 @@ "APPEARANCE": "Erscheinungsbild", "OTHER": "Anderes", "STORAGE": "Speicher" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { @@ -1399,7 +1543,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1490,6 +1635,16 @@ "ACTIONS_DESCRIPTION": "Aktionen v2 ermöglichen die Verwaltung von Datenausführungen und Zielen. Wenn das Flag aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Sitzungsbeendigung", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Wenn das Flag aktiviert ist, können Sie eine einzelne Sitzung über die Login-Benutzeroberfläche beenden, indem Sie einen id_token mit einem `sid` Claim als id_token_hint am Endpunkt end_session übergeben. Beachten Sie, dass derzeit alle Sitzungen desselben Benutzeragenten (Browser) in der Login-Benutzeroberfläche beendet werden. Sitzungen, die über die Session API verwaltet werden, ermöglichen bereits die Beendigung einzelner Sitzungen.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Parent error", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Wenn die Flagge aktiviert ist, wird der OIDC-Elternfehler in der Konsole protokolliert.", + "DISABLEUSERTOKENEVENT": "Benutzer-Token-Ereignis deaktivieren", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Backchannel-Logout aktivieren", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Der Back-Channel-Logout implementiert OpenID Connect Back-Channel Logout 1.0 und kann verwendet werden, um Clients über die Beendigung der Sitzung beim OpenID-Provider zu benachrichtigen.", + "PERMISSIONCHECKV2": "Berechtigungsprüfung V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Wenn die Flagge aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", + "WEBKEY": "Web-Schlüssel", + "WEBKEY_DESCRIPTION": "Wenn die Flagge aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", "STATES": { "INHERITED": "Erben", "ENABLED": "Aktiviert", @@ -1502,7 +1657,10 @@ }, "RESET": "Alle auf Erben setzen", "CONSOLEUSEV2USERAPI": "Verwende die V2-API in der Konsole zur Erstellung von Benutzern", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Wenn diese Option aktiviert ist, verwendet die Konsole die V2 User API, um neue Benutzer zu erstellen. Mit der V2 API starten neu erstellte Benutzer nicht im Initial Zustand." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Wenn diese Option aktiviert ist, verwendet die Konsole die V2 User API, um neue Benutzer zu erstellen. Mit der V2 API starten neu erstellte Benutzer nicht im Initial Zustand.", + "LOGINV2": "Login V2", + "LOGINV2_DESCRIPTION": "Durch das Aktivieren wird das neue TypeScript-basierte Login-UI mit verbesserter Sicherheit, Leistung und Anpassbarkeit aktiviert.", + "LOGINV2_BASEURI": "Basis-URI" }, "DIALOG": { "RESET": { @@ -1639,7 +1797,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Email Verification erfolgreich", @@ -2588,7 +2748,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Manager hinzufügen", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 5f36a64131..5e2cc3f4c9 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flows", "DESCRIPTION": "Choose an authentication flow and trigger your action on a specific event within this flow." - } + }, + "ACTIONSTWO_NOTE": "Actions V2 a new, improved version of Actions is now available. The current version is still accessible, but our future development will focus on the new one, which will eventually replace the current version." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "The idle refresh token lifetime is the maximum time a refresh token can be unused." } }, + "WEB_KEYS": { + "DESCRIPTION": "Manage your OIDC Web Keys to securely sign and validate tokens for your ZITADEL instance.", + "TABLE": { + "TITLE": "Active and Future Web Keys", + "DESCRIPTION": "Your active and upcoming web keys. Activating a new key will deactivate the current one.", + "NOTE": "Note: The JWKs OIDC endpoint returns a cacheable response (default 5 min). Avoid activating a key too soon, as it may not be available to caches and clients yet.", + "ACTIVATE": "Activate next Web Key", + "ACTIVE": "Currently active", + "NEXT": "Next in queue", + "FUTURE": "Future", + "WARNING": "Web Key is less than 5 min old" + }, + "CREATE": { + "TITLE": "Create new Web Key", + "DESCRIPTION": "Creating a new web key adds it to your list. ZITADEL uses RSA2048 keys with a SHA256 hasher by default.", + "KEY_TYPE": "Key Type", + "BITS": "Bits", + "HASHER": "Hasher", + "CURVE": "Curve" + }, + "PREVIOUS_TABLE": { + "TITLE": "Previous Web Keys", + "DESCRIPTION": "These are your previous web keys that are no longer active.", + "DEACTIVATED_ON": "Deactivated on" + } + }, "MESSAGE_TEXTS": { "TITLE": "Message Texts", "DESCRIPTION": "Customize the texts of your notification email or SMS messages. If you want to disable some of the languages, restrict them in your instances language settings.", @@ -502,6 +529,118 @@ "DOWNLOAD": "Download", "APPLY": "Apply" }, + "ACTIONSTWO": { + "BETA_NOTE": "You are currently using the new Actions V2, which is in beta. The previous Version 1 is still available but will be discontinued in the future. Please report any issues or feedback.", + "EXECUTION": { + "TITLE": "Actions", + "DESCRIPTION": "Actions let you run custom code in response to API requests, events or specific functions. Use them to extend Zitadel, automate workflows, and itegrate with other systems.", + "TYPES": { + "request": "Request", + "response": "Response", + "event": "Events", + "function": "Function" + }, + "DIALOG": { + "CREATE_TITLE": "Create an Action", + "UPDATE_TITLE": "Update an Action", + "TYPE": { + "DESCRIPTION": "Select when you want this Action to run", + "REQUEST": { + "TITLE": "Request", + "DESCRIPTION": "Requests that occur within Zitadel. This could be something as a login request call." + }, + "RESPONSE": { + "TITLE": "Response", + "DESCRIPTION": "A response from a request within Zitadel. Think of the response you get back from fetching a user." + }, + "EVENTS": { + "TITLE": "Events", + "DESCRIPTION": "Events that happen within Zitadel. This could be anything like a user creating an account, a successful login etc." + }, + "FUNCTIONS": { + "TITLE": "Functions", + "DESCRIPTION": "Functions that you can call within Zitadel. This could be anything from sending an email to creating a user." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Choose whether this action applies to all request, a specific service (ec. user management), or a single request (e.g. create user).", + "ALL": { + "TITLE": "All", + "DESCRIPTION": "Select this if you want to run your action on every request" + }, + "ALL_EVENTS": "Select this if you want to run your action on every event", + "SELECT_SERVICE": { + "TITLE": "Select Service", + "DESCRIPTION": "Choose a Zitadel Service for you action." + }, + "SELECT_METHOD": { + "TITLE": "Select Method", + "DESCRIPTION": "If you want to only execute on a specific request, select it here", + "NOTE": "If you don't select a method, your action will run on every request in your selected service." + }, + "FUNCTIONNAME": { + "TITLE": "Function Name", + "DESCRIPTION": "Choose the function you want to execute" + }, + "SELECT_GROUP": { + "TITLE": "Set Group", + "DESCRIPTION": "If you want to only execute on a group of events, set the group here" + }, + "SELECT_EVENT": { + "TITLE": "Select Event", + "DESCRIPTION": "If you want to only execute on a specific event, specify it here" + } + }, + "TARGET": { + "DESCRIPTION": "You can choose to execute a target, or to run it on the same conditions as other targets.", + "TARGET": { + "DESCRIPTION": "The target you want to execute for this action" + }, + "CONDITIONS": { + "DESCRIPTION": "Execution Conditions" + } + } + }, + "TABLE": { + "CONDITION": "Condition", + "TYPE": "Type", + "TARGET": "Target", + "CREATIONDATE": "Creation Date" + } + }, + "TARGET": { + "TITLE": "Targets", + "DESCRIPTION": "A target is the destination of the code you want to execute from an action. Create a target here and at it to your actions.", + "CREATE": { + "TITLE": "Create your Target", + "DESCRIPTION": "Create your own target outside of Zitadel", + "NAME": "Name", + "NAME_DESCRIPTION": "Give your target a clear, descriptive name to make it easy to identify later", + "TYPE": "Type", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Call", + "restAsync": "REST Async" + }, + "TYPES_DESCRIPTION": "Webhook, the call handles the status code but response is irrelevant\nCall, the call handles the status code and response\nAsync, the call handles neither status code nor response, but can be called in parallel with other Targets", + "ENDPOINT": "Endpoint", + "ENDPOINT_DESCRIPTION": "Enter the endpoint where your code is hosted. Make sure it is accessible to us!", + "TIMEOUT": "Timeout", + "TIMEOUT_DESCRIPTION": "Set the maximum time your target has to respond. If it takes longer, we will stop the request.", + "INTERRUPT_ON_ERROR": "Interrupt on Error", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Stop all executions when the targets returns with an error", + "INTERRUPT_ON_ERROR_WARNING": "Caution: “Interrupt on Error” halts operations on failure, risking lockout. Test with it disabled to prevent blocking login/creation.", + "AWAIT_RESPONSE": "Await Response", + "AWAIT_RESPONSE_DESCRIPTION": "We'll Wait for a response before we do anything else. Useful if you intend to use multiple targets for a single action" + }, + "TABLE": { + "NAME": "Name", + "ENDPOINT": "Endpoint", + "CREATIONDATE": "Creation Date", + "REORDER": "Reorder" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Has control over the whole instance, including all organizations", "IAM_OWNER_VIEWER": "Has permission to review the whole instance, including all organizations", @@ -790,7 +929,10 @@ "PHONESECTION": "Phone numbers", "PASSWORDSECTION": "Initial Password", "ADDRESSANDPHONESECTION": "Phone number", - "INITMAILDESCRIPTION": "If both options are selected, no email for initialization will be sent. If only one of the options is selected, a mail to provide / verify the data will be sent." + "INITMAILDESCRIPTION": "If both options are selected, no email for initialization will be sent. If only one of the options is selected, a mail to provide / verify the data will be sent.", + "SETUPAUTHENTICATIONLATER": "Setup authentication later for this User.", + "INVITATION": "Send an invitation E-Mail for authentication setup and E-Mail verification.", + "INITIALPASSWORD": "Set an initial password for the User." }, "CODEDIALOG": { "TITLE": "Verify Phone Number", @@ -1354,11 +1496,14 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "External links", "OIDC": "OIDC Token lifetime and expiration", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Secret Generator", "SECURITY": "Security settings", "EVENTS": "Events", "FAILEDEVENTS": "Failed Events", - "VIEWS": "Views" + "VIEWS": "Views", + "ACTIONS": "Actions", + "TARGETS": "Targets" }, "GROUPS": { "GENERAL": "General Information", @@ -1368,8 +1513,10 @@ "TEXTS": "Texts and Languages", "APPEARANCE": "Appearance", "OTHER": "Other", - "STORAGE": "Storage" - } + "STORAGE": "Storage", + "ACTIONS": "Actions" + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { @@ -1399,7 +1546,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1490,6 +1638,16 @@ "ACTIONS_DESCRIPTION": "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Session Termination", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "If the flag is enabled, you'll be able to terminate a single session from the login UI by providing an id_token with a `sid` claim as id_token_hint on the end_session endpoint. Note that currently all sessions from the same user agent (browser) are terminated in the login UI. Sessions managed through the Session API already allow the termination of single sessions.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Parent Error", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "If the flag is enabled, the OIDC parent error will be logged in the console.", + "DISABLEUSERTOKENEVENT": "Disable User Token Event", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Enable Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "The Back-Channel Logout implements OpenID Connect Back-Channel Logout 1.0 and can be used to notify clients about session termination at the OpenID Provider.", + "PERMISSIONCHECKV2": "Permission Check V2", + "PERMISSIONCHECKV2_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", + "WEBKEY": "Web Key", + "WEBKEY_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", "STATES": { "INHERITED": "Inherit", "ENABLED": "Enabled", @@ -1502,7 +1660,10 @@ }, "RESET": "Set all to inherit", "CONSOLEUSEV2USERAPI": "Use V2 Api in Console for User creation", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "When this flag is enabled, the console uses the V2 User API to create new users. With the V2 API, newly created users start without an initial state." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "When this flag is enabled, the console uses the V2 User API to create new users. With the V2 API, newly created users start without an initial state.", + "LOGINV2": "Login V2", + "LOGINV2_DESCRIPTION": "Enabling this activates the new TypeScript-based login UI with improved security, performance, and customization.", + "LOGINV2_BASEURI": "Base URI" }, "DIALOG": { "RESET": { @@ -1639,7 +1800,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Email verification done", @@ -2613,7 +2776,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Add a Manager", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 23315df897..198bb3ca8b 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flujos", "DESCRIPTION": "Elige un flujo de autenticación y activa tu acción en un evento específico dentro de este flujo." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, una nueva y mejorada versión de Actions, ya está disponible. La versión actual sigue siendo accesible, pero nuestro desarrollo futuro se centrará en la nueva, que acabará reemplazando la versión actual." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "La duración de vida del token de actualización en espera es el tiempo máximo que un token de actualización puede estar sin usar." } }, + "WEB_KEYS": { + "DESCRIPTION": "Administra tus claves web OIDC para firmar y validar tokens de manera segura en tu instancia de ZITADEL.", + "TABLE": { + "TITLE": "Claves Web Activas y Futuras", + "DESCRIPTION": "Tus claves web activas y próximas. Activar una nueva clave desactivará la actual.", + "NOTE": "Nota: El endpoint JWKs OIDC devuelve una respuesta almacenable en caché (por defecto 5 min). Evita activar una clave demasiado pronto, ya que puede que aún no esté disponible en cachés y clientes.", + "ACTIVATE": "Activar la siguiente Clave Web", + "ACTIVE": "Actualmente activa", + "NEXT": "Siguiente en la cola", + "FUTURE": "Futuro", + "WARNING": "La clave web tiene menos de 5 minutos" + }, + "CREATE": { + "TITLE": "Crear nueva Clave Web", + "DESCRIPTION": "Crear una nueva clave web la añadirá a tu lista. ZITADEL usa por defecto claves RSA2048 con un algoritmo de hash SHA256.", + "KEY_TYPE": "Tipo de Clave", + "BITS": "Bits", + "HASHER": "Algoritmo de Hash", + "CURVE": "Curva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Claves Web Anteriores", + "DESCRIPTION": "Estas son tus claves web anteriores que ya no están activas.", + "DEACTIVATED_ON": "Desactivada el" + } + }, "MESSAGE_TEXTS": { "TITLE": "Textos de Mensajes", "DESCRIPTION": "Personaliza los textos de tus mensajes de correo electrónico de notificación o mensajes SMS. Si deseas desactivar algunos de los idiomas, restríngelos en la configuración de idiomas de tus instancias.", @@ -502,6 +529,118 @@ "DOWNLOAD": "Descargar", "APPLY": "Aplicar" }, + "ACTIONSTWO": { + "BETA_NOTE": "Actualmente estás usando la nueva versión Actions V2, que está en fase beta. La versión anterior 1 todavía está disponible, pero será descontinuada en el futuro. Por favor, informa de cualquier problema o comentario.", + "EXECUTION": { + "TITLE": "Acciones", + "DESCRIPTION": "Las acciones te permiten ejecutar código personalizado en respuesta a solicitudes de API, eventos o funciones específicas. Úsalas para extender Zitadel, automatizar flujos de trabajo e integrarte con otros sistemas.", + "TYPES": { + "request": "Solicitud", + "response": "Respuesta", + "event": "Eventos", + "function": "Función" + }, + "DIALOG": { + "CREATE_TITLE": "Crear una acción", + "UPDATE_TITLE": "Actualizar una acción", + "TYPE": { + "DESCRIPTION": "Selecciona cuándo quieres que se ejecute esta acción", + "REQUEST": { + "TITLE": "Solicitud", + "DESCRIPTION": "Solicitudes que ocurren dentro de Zitadel. Esto podría ser algo como una llamada de solicitud de inicio de sesión." + }, + "RESPONSE": { + "TITLE": "Respuesta", + "DESCRIPTION": "Una respuesta de una solicitud dentro de Zitadel. Piensa en la respuesta que recibes al obtener un usuario." + }, + "EVENTS": { + "TITLE": "Eventos", + "DESCRIPTION": "Eventos que ocurren dentro de Zitadel. Esto podría ser cualquier cosa como un usuario creando una cuenta, un inicio de sesión exitoso, etc." + }, + "FUNCTIONS": { + "TITLE": "Funciones", + "DESCRIPTION": "Funciones que puedes llamar dentro de Zitadel. Esto podría ser cualquier cosa, desde enviar un correo electrónico hasta crear un usuario." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Elige si esta acción se aplica a todas las solicitudes, a un servicio específico (p. ej., administración de usuarios) o a una sola solicitud (p. ej., crear usuario).", + "ALL": { + "TITLE": "Todas", + "DESCRIPTION": "Selecciona esto si quieres ejecutar tu acción en cada solicitud" + }, + "ALL_EVENTS": "Selecciona esto si quieres ejecutar tu acción en cada evento", + "SELECT_SERVICE": { + "TITLE": "Seleccionar servicio", + "DESCRIPTION": "Elige un servicio de Zitadel para tu acción." + }, + "SELECT_METHOD": { + "TITLE": "Seleccionar método", + "DESCRIPTION": "Si quieres ejecutar solo en una solicitud específica, selecciónala aquí", + "NOTE": "Si no seleccionas un método, tu acción se ejecutará en cada solicitud de tu servicio seleccionado." + }, + "FUNCTIONNAME": { + "TITLE": "Nombre de la función", + "DESCRIPTION": "Elige la función que quieres ejecutar" + }, + "SELECT_GROUP": { + "TITLE": "Establecer grupo", + "DESCRIPTION": "Si quieres ejecutar solo en un grupo de eventos, establece el grupo aquí" + }, + "SELECT_EVENT": { + "TITLE": "Seleccionar evento", + "DESCRIPTION": "Si quieres ejecutar solo en un evento específico, especifícalo aquí" + } + }, + "TARGET": { + "DESCRIPTION": "Puedes elegir ejecutar un objetivo o ejecutarlo en las mismas condiciones que otros objetivos.", + "TARGET": { + "DESCRIPTION": "El objetivo que quieres ejecutar para esta acción" + }, + "CONDITIONS": { + "DESCRIPTION": "Condiciones de ejecución" + } + } + }, + "TABLE": { + "CONDITION": "Condición", + "TYPE": "Tipo", + "TARGET": "Objetivo", + "CREATIONDATE": "Fecha de creación" + } + }, + "TARGET": { + "TITLE": "Objetivos", + "DESCRIPTION": "Un objetivo es el destino del código que quieres ejecutar desde una acción. Crea un objetivo aquí y agrégalo a tus acciones.", + "CREATE": { + "TITLE": "Crear tu objetivo", + "DESCRIPTION": "Crea tu propio objetivo fuera de Zitadel", + "NAME": "Nombre", + "NAME_DESCRIPTION": "Dale a tu objetivo un nombre claro y descriptivo para que sea fácil de identificar más tarde", + "TYPE": "Tipo", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Llamada REST", + "restAsync": "REST Asíncrono" + }, + "TYPES_DESCRIPTION": "Webhook, la llamada maneja el código de estado pero la respuesta es irrelevante\nCall, la llamada maneja el código de estado y la respuesta\nAsync, la llamada no maneja ni el código de estado ni la respuesta, pero puede ser llamada en paralelo con otros objetivos", + "ENDPOINT": "Punto de conexión", + "ENDPOINT_DESCRIPTION": "Introduce el punto de conexión donde se aloja tu código. ¡Asegúrate de que sea accesible para nosotros!", + "TIMEOUT": "Tiempo de espera", + "TIMEOUT_DESCRIPTION": "Establece el tiempo máximo que tiene tu objetivo para responder. Si tarda más, detendremos la solicitud.", + "INTERRUPT_ON_ERROR": "Interrumpir en error", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Detén todas las ejecuciones cuando los objetivos devuelvan un error", + "INTERRUPT_ON_ERROR_WARNING": "Precaución: “Interrumpir en error” detiene las operaciones si fallan, lo que puede provocar un bloqueo. Pruebe con esta opción desactivada para evitar bloquear el inicio de sesión o la creación.", + "AWAIT_RESPONSE": "Esperar respuesta", + "AWAIT_RESPONSE_DESCRIPTION": "Esperaremos una respuesta antes de hacer cualquier otra cosa. Útil si tienes la intención de usar múltiples objetivos para una sola acción" + }, + "TABLE": { + "NAME": "Nombre", + "ENDPOINT": "Punto de conexión", + "CREATIONDATE": "Fecha de creación", + "REORDER": "Reordenar" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Tiene control sobre toda la instancia, incluyendo todas las organizaciones", "IAM_OWNER_VIEWER": "Tiene permiso para revisar toda la instancia, incluyendo todas las organizaciones", @@ -790,7 +929,10 @@ "PHONESECTION": "Números de teléfono", "PASSWORDSECTION": "Contraseña inicial", "ADDRESSANDPHONESECTION": "Número de teléfono", - "INITMAILDESCRIPTION": "Si ambas opciones se seleccionan, no se enviará un email para la inicialización. Si solo una de las opciones se selecciona, un email se enviará para proporcionar / verificar los datos." + "INITMAILDESCRIPTION": "Si ambas opciones se seleccionan, no se enviará un email para la inicialización. Si solo una de las opciones se selecciona, un email se enviará para proporcionar / verificar los datos.", + "SETUPAUTHENTICATIONLATER": "Configurar la autenticación más tarde para este usuario.", + "INVITATION": "Enviar un correo de invitación para la configuración de autenticación y verificación de correo electrónico.", + "INITIALPASSWORD": "Establece una contraseña inicial para el usuario." }, "CODEDIALOG": { "TITLE": "Verificar número de teléfono", @@ -1355,6 +1497,7 @@ "BRANDING": "Imagen de marca", "PRIVACYPOLICY": "Política de privacidad", "OIDC": "OIDC Token lifetime and expiration", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Apariencia del secreto", "SECURITY": "Ajustes de seguridad", "EVENTS": "Eventos", @@ -1370,7 +1513,8 @@ "APPEARANCE": "Apariencia", "OTHER": "Otros", "STORAGE": "Datos" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { @@ -1400,7 +1544,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1491,6 +1636,16 @@ "ACTIONS_DESCRIPTION": "Acciones v2 permite administrar las ejecuciones y objetivos de datos. Si la bandera está habilitada, podrá utilizar la nueva API y sus funciones.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Terminación de sesión", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Si la bandera está habilitada, podrá terminar una sesión única desde la interfaz de usuario de inicio de sesión proporcionando un id_token con una reclamación `sid` como id_token_hint en el punto final de end_session. Tenga en cuenta que actualmente se terminan todas las sesiones del mismo agente de usuario (navegador) en la interfaz de usuario de inicio de sesión. Las sesiones administradas a través de la API de sesión ya permiten la terminación de sesiones individuales.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Parent Error", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Si la bandera está habilitada, el error del padre OIDC se registrará en la consola.", + "DISABLEUSERTOKENEVENT": "Desactivar evento de token de usuario", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Habilitar Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "El Back-Channel Logout implementa OpenID Connect Back-Channel Logout 1.0 y se puede usar para notificar a los clientes sobre la terminación de la sesión en el proveedor de OpenID.", + "PERMISSIONCHECKV2": "Verificación de permisos V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Si la bandera está habilitada, podrá usar la nueva API y sus funciones.", + "WEBKEY": "Clave web", + "WEBKEY_DESCRIPTION": "Si la bandera está habilitada, podrá usar la nueva API y sus funciones.", "STATES": { "INHERITED": "Heredado", "ENABLED": "Habilitado", @@ -1503,7 +1658,10 @@ }, "RESET": "Establecer todo a heredado", "CONSOLEUSEV2USERAPI": "Utilice la API V2 en la consola para la creación de usuarios", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Cuando esta opción está habilitada, la consola utiliza la API V2 de usuario para crear nuevos usuarios. Con la API V2, los usuarios recién creados comienzan sin un estado inicial." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Cuando esta opción está habilitada, la consola utiliza la API V2 de usuario para crear nuevos usuarios. Con la API V2, los usuarios recién creados comienzan sin un estado inicial.", + "LOGINV2": "Inicio de sesión V2", + "LOGINV2_DESCRIPTION": "Al habilitar esto, se activa la nueva interfaz de inicio de sesión basada en TypeScript con mejoras en seguridad, rendimiento y personalización.", + "LOGINV2_BASEURI": "URI base" }, "DIALOG": { "RESET": { @@ -1640,7 +1798,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Verificación de email realizada", @@ -2585,7 +2745,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Añadir un Mánager", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index b516561077..0d66c4193e 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flux", "DESCRIPTION": "Choisissez un flux d'authentification et déclenchez votre action sur un événement spécifique dans ce flux." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, une nouvelle version améliorée de Actions, est désormais disponible. La version actuelle reste accessible, mais notre développement futur se concentrera sur la nouvelle, qui finira par remplacer la version actuelle." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "La durée de vie du token de rafraîchissement inactif est le temps maximum qu'un token de rafraîchissement peut être inutilisé." } }, + "WEB_KEYS": { + "DESCRIPTION": "Gérez vos clés Web OIDC pour signer et valider en toute sécurité les jetons de votre instance ZITADEL.", + "TABLE": { + "TITLE": "Clés Web Actives et Futures", + "DESCRIPTION": "Vos clés Web actives et à venir. L'activation d'une nouvelle clé désactivera l'actuelle.", + "NOTE": "Remarque : Le point de terminaison JWKs OIDC renvoie une réponse mise en cache (par défaut 5 min). Évitez d'activer une clé trop tôt, car elle pourrait ne pas encore être disponible pour les caches et les clients.", + "ACTIVATE": "Activer la prochaine Clé Web", + "ACTIVE": "Actuellement active", + "NEXT": "Prochaine dans la file d'attente", + "FUTURE": "Futur", + "WARNING": "La clé Web a moins de 5 minutes" + }, + "CREATE": { + "TITLE": "Créer une nouvelle Clé Web", + "DESCRIPTION": "Créer une nouvelle clé Web l'ajoutera à votre liste. ZITADEL utilise par défaut des clés RSA2048 avec un hacheur SHA256.", + "KEY_TYPE": "Type de Clé", + "BITS": "Bits", + "HASHER": "Hacheur", + "CURVE": "Courbe" + }, + "PREVIOUS_TABLE": { + "TITLE": "Clés Web Précédentes", + "DESCRIPTION": "Voici vos anciennes clés Web qui ne sont plus actives.", + "DEACTIVATED_ON": "Désactivée le" + } + }, "MESSAGE_TEXTS": { "TITLE": "Textes des Messages", "DESCRIPTION": "Personnalisez les textes de vos e-mails de notification ou messages SMS. Si vous souhaitez désactiver certaines langues, restreignez-les dans les paramètres de langue de vos instances.", @@ -502,6 +529,118 @@ "DOWNLOAD": "Télécharger", "APPLY": "Appliquer" }, + "ACTIONSTWO": { + "BETA_NOTE": "Vous utilisez actuellement la nouvelle version Actions V2, qui est en phase bêta. L'ancienne version 1 est toujours disponible mais sera arrêtée à l'avenir. Veuillez signaler tout problème ou commentaire.", + "EXECUTION": { + "TITLE": "Actions", + "DESCRIPTION": "Les actions vous permettent d'exécuter du code personnalisé en réponse à des requêtes API, des événements ou des fonctions spécifiques. Utilisez-les pour étendre Zitadel, automatiser les flux de travail et vous intégrer à d'autres systèmes.", + "TYPES": { + "request": "Requête", + "response": "Réponse", + "event": "Événements", + "function": "Fonction" + }, + "DIALOG": { + "CREATE_TITLE": "Créer une action", + "UPDATE_TITLE": "Mettre à jour une action", + "TYPE": { + "DESCRIPTION": "Sélectionnez quand vous souhaitez que cette action s'exécute", + "REQUEST": { + "TITLE": "Requête", + "DESCRIPTION": "Requêtes qui se produisent dans Zitadel. Cela pourrait être quelque chose comme un appel de requête de connexion." + }, + "RESPONSE": { + "TITLE": "Réponse", + "DESCRIPTION": "Une réponse à une requête dans Zitadel. Pensez à la réponse que vous obtenez lorsque vous récupérez un utilisateur." + }, + "EVENTS": { + "TITLE": "Événements", + "DESCRIPTION": "Événements qui se produisent dans Zitadel. Cela pourrait être n'importe quoi, comme un utilisateur créant un compte, une connexion réussie, etc." + }, + "FUNCTIONS": { + "TITLE": "Fonctions", + "DESCRIPTION": "Fonctions que vous pouvez appeler dans Zitadel. Cela pourrait être n'importe quoi, de l'envoi d'un e-mail à la création d'un utilisateur." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Choisissez si cette action s'applique à toutes les requêtes, à un service spécifique (par exemple, la gestion des utilisateurs) ou à une seule requête (par exemple, créer un utilisateur).", + "ALL": { + "TITLE": "Tous", + "DESCRIPTION": "Sélectionnez ceci si vous souhaitez exécuter votre action sur chaque requête" + }, + "ALL_EVENTS": "Sélectionnez ceci si vous souhaitez exécuter votre action à chaque événement", + "SELECT_SERVICE": { + "TITLE": "Sélectionner un service", + "DESCRIPTION": "Choisissez un service Zitadel pour votre action." + }, + "SELECT_METHOD": { + "TITLE": "Sélectionner une méthode", + "DESCRIPTION": "Si vous souhaitez exécuter uniquement sur une requête spécifique, sélectionnez-la ici", + "NOTE": "Si vous ne sélectionnez pas de méthode, votre action s'exécutera sur chaque requête de votre service sélectionné." + }, + "FUNCTIONNAME": { + "TITLE": "Nom de la fonction", + "DESCRIPTION": "Choisissez la fonction que vous souhaitez exécuter" + }, + "SELECT_GROUP": { + "TITLE": "Définir un groupe", + "DESCRIPTION": "Si vous souhaitez exécuter uniquement sur un groupe d'événements, définissez le groupe ici" + }, + "SELECT_EVENT": { + "TITLE": "Sélectionner un événement", + "DESCRIPTION": "Si vous souhaitez exécuter uniquement sur un événement spécifique, spécifiez-le ici" + } + }, + "TARGET": { + "DESCRIPTION": "Vous pouvez choisir d'exécuter une cible ou de l'exécuter dans les mêmes conditions que d'autres cibles.", + "TARGET": { + "DESCRIPTION": "La cible que vous souhaitez exécuter pour cette action" + }, + "CONDITIONS": { + "DESCRIPTION": "Conditions d'exécution" + } + } + }, + "TABLE": { + "CONDITION": "Condition", + "TYPE": "Type", + "TARGET": "Cible", + "CREATIONDATE": "Date de création" + } + }, + "TARGET": { + "TITLE": "Cibles", + "DESCRIPTION": "Une cible est la destination du code que vous souhaitez exécuter à partir d'une action. Créez une cible ici et ajoutez-la à vos actions.", + "CREATE": { + "TITLE": "Créer votre cible", + "DESCRIPTION": "Créez votre propre cible en dehors de Zitadel", + "NAME": "Nom", + "NAME_DESCRIPTION": "Donnez à votre cible un nom clair et descriptif pour la rendre facile à identifier plus tard", + "TYPE": "Type", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Appel REST", + "restAsync": "REST Asynchrone" + }, + "TYPES_DESCRIPTION": "Webhook, l'appel gère le code d'état mais la réponse est sans importance\nCall, l'appel gère le code d'état et la réponse\nAsync, l'appel ne gère ni le code d'état ni la réponse, mais peut être appelé en parallèle avec d'autres cibles", + "ENDPOINT": "Point de terminaison", + "ENDPOINT_DESCRIPTION": "Entrez le point de terminaison où votre code est hébergé. Assurez-vous qu'il nous est accessible !", + "TIMEOUT": "Délai d'attente", + "TIMEOUT_DESCRIPTION": "Définissez le temps maximal dont votre cible dispose pour répondre. Si cela prend plus de temps, nous arrêterons la requête.", + "INTERRUPT_ON_ERROR": "Interrompre en cas d'erreur", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Arrêtez toutes les exécutions lorsque les cibles renvoient une erreur", + "INTERRUPT_ON_ERROR_WARNING": "Attention : “Interrompre en cas d'erreur” arrête les opérations en cas d’échec, ce qui peut entraîner un verrouillage. Testez avec cette option désactivée pour éviter de bloquer la connexion ou la création.", + "AWAIT_RESPONSE": "Attendre une réponse", + "AWAIT_RESPONSE_DESCRIPTION": "Nous attendrons une réponse avant de faire autre chose. Utile si vous avez l'intention d'utiliser plusieurs cibles pour une seule action" + }, + "TABLE": { + "NAME": "Nom", + "ENDPOINT": "Point de terminaison", + "CREATIONDATE": "Date de création", + "REORDER": "Réorganiser" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "A le contrôle de toute l'instance, y compris toutes les organisations", "IAM_OWNER_VIEWER": "A le droit de passer en revue l'ensemble de l'instance, y compris toutes les organisations.", @@ -790,7 +929,10 @@ "PHONESECTION": "Numéro de téléphone", "PASSWORDSECTION": "Mot de passe initial", "ADDRESSANDPHONESECTION": "Numéro de téléphone", - "INITMAILDESCRIPTION": "Si les deux options sont sélectionnées, aucun mail d'initialisation ne sera envoyé. Si une seule des options est sélectionnée, un mail pour fournir / vérifier les données sera envoyé." + "INITMAILDESCRIPTION": "Si les deux options sont sélectionnées, aucun mail d'initialisation ne sera envoyé. Si une seule des options est sélectionnée, un mail pour fournir / vérifier les données sera envoyé.", + "SETUPAUTHENTICATIONLATER": "Configurer l'authentification plus tard pour cet utilisateur.", + "INVITATION": "Envoyer un e-mail d'invitation pour la configuration de l'authentification et la vérification de l'e-mail.", + "INITIALPASSWORD": "Définissez un mot de passe initial pour l'utilisateur." }, "CODEDIALOG": { "TITLE": "Vérifier le numéro de téléphone", @@ -1354,6 +1496,7 @@ "BRANDING": "Image de marque", "PRIVACYPOLICY": "Politique de confidentialité", "OIDC": "Durée de vie et expiration des jetons OIDC", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Générateur de secrets", "SECURITY": "Paramètres de sécurité", "EVENTS": "Événements", @@ -1369,7 +1512,8 @@ "APPEARANCE": "Apparence", "OTHER": "Autres", "STORAGE": "Stockage" - } + }, + "BETA": "BÊTA" }, "SETTING": { "LANGUAGES": { @@ -1399,7 +1543,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1490,6 +1635,16 @@ "ACTIONS_DESCRIPTION": "Les actions v2 permettent de gérer les exécutions et les cibles de données. Si l'indicateur est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Fin de session", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Si l'indicateur est activé, vous pourrez terminer une seule session à partir de l'interface utilisateur de connexion en fournissant un id_token avec une revendication `sid` en tant que id_token_hint sur le point de terminaison end_session. Notez que toutes les sessions du même agent utilisateur (navigateur) sont actuellement terminées dans l'interface utilisateur de connexion. Les sessions gérées via l'API de session permettent déjà la terminaison de sessions individuelles.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Parent Error", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Si le drapeau est activé, l'erreur parent OIDC sera enregistrée dans la console.", + "DISABLEUSERTOKENEVENT": "Désactiver l'événement de jeton utilisateur", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Activer le Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Le Back-Channel Logout implémente OpenID Connect Back-Channel Logout 1.0 et peut être utilisé pour notifier les clients de la fin de session chez le fournisseur OpenID.", + "PERMISSIONCHECKV2": "Vérification des permissions V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Si le drapeau est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", + "WEBKEY": "Clé web", + "WEBKEY_DESCRIPTION": "Si le drapeau est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", "STATES": { "INHERITED": "Hérité", "ENABLED": "Activé", @@ -1502,7 +1657,10 @@ }, "RESET": "Réinitialiser tout sur hérité", "CONSOLEUSEV2USERAPI": "Utilisez l'API V2 dans la console pour la création d'utilisateurs", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Lorsque ce drapeau est activé, la console utilise l'API V2 User pour créer de nouveaux utilisateurs. Avec l'API V2, les nouveaux utilisateurs commencent sans état initial." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Lorsque ce drapeau est activé, la console utilise l'API V2 User pour créer de nouveaux utilisateurs. Avec l'API V2, les nouveaux utilisateurs commencent sans état initial.", + "LOGINV2": "Connexion V2", + "LOGINV2_DESCRIPTION": "L’activation de cette option lance la nouvelle interface de connexion basée sur TypeScript, avec une sécurité, des performances et une personnalisation améliorées.", + "LOGINV2_BASEURI": "URI de base" }, "DIALOG": { "RESET": { @@ -1639,7 +1797,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Vérification de l'e-mail effectuée", @@ -2589,7 +2749,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Ajouter un responsable", diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json index b4633323aa..96d1fe16df 100644 --- a/console/src/assets/i18n/hu.json +++ b/console/src/assets/i18n/hu.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Folyamatok", "DESCRIPTION": "Válassz egy hitelesítési folyamatot, és váltasd ki a műveletedet egy adott esemény bekövetkezésekor ebben a folyamatban." - } + }, + "ACTIONSTWO_NOTE": "Az Actions V2, az Actions új, továbbfejlesztett verziója mostantól elérhető. A jelenlegi verzió továbbra is elérhető, de a jövőbeli fejlesztéseink az új verzióra összpontosítanak, amely végül felváltja a jelenlegi verziót." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "A tétlen frissítő token élettartama az a maximális idő, ameddig a frissítő token használaton kívül maradhat." } }, + "WEB_KEYS": { + "DESCRIPTION": "Kezelje az OIDC Webkulcsokat, hogy biztonságosan aláírja és érvényesítse a tokeneket a ZITADEL példányában.", + "TABLE": { + "TITLE": "Aktív és Jövőbeli Webkulcsok", + "DESCRIPTION": "Az aktív és közelgő webkulcsai. Egy új kulcs aktiválása deaktiválja az aktuálisat.", + "NOTE": "Megjegyzés: A JWKs OIDC végpont egy gyorsítótárazható választ ad vissza (alapértelmezett: 5 perc). Kerülje a kulcs túl korai aktiválását, mivel lehet, hogy még nem érhető el a gyorsítótárakban és a klienseknél.", + "ACTIVATE": "Következő Webkulcs aktiválása", + "ACTIVE": "Jelenleg aktív", + "NEXT": "Következő a sorban", + "FUTURE": "Jövőbeli", + "WARNING": "A webkulcs kevesebb mint 5 perces" + }, + "CREATE": { + "TITLE": "Új Webkulcs létrehozása", + "DESCRIPTION": "Egy új webkulcs létrehozása hozzáadja azt a listájához. A ZITADEL alapértelmezés szerint RSA2048 kulcsokat használ SHA256 hasheléssel.", + "KEY_TYPE": "Kulcstípus", + "BITS": "Bitek", + "HASHER": "Hasher", + "CURVE": "Görbe" + }, + "PREVIOUS_TABLE": { + "TITLE": "Korábbi Webkulcsok", + "DESCRIPTION": "Ezek a korábbi webkulcsai, amelyek már nem aktívak.", + "DEACTIVATED_ON": "Deaktiválva ekkor" + } + }, "MESSAGE_TEXTS": { "TITLE": "Üzenet Szövegek", "DESCRIPTION": "Testreszabhatod az értesítési e-mailjeid vagy SMS üzeneteid szövegeit. Ha le szeretnél tiltani néhány nyelvet, korlátozd azokat az instance nyelvi beállításaiban.", @@ -502,6 +529,118 @@ "DOWNLOAD": "Letöltés", "APPLY": "Alkalmaz" }, + "ACTIONSTWO": { + "BETA_NOTE": "Jelenleg az új Actions V2-t használja, amely béta verzióban van. Az előző 1-es verzió továbbra is elérhető, de a jövőben megszűnik. Kérjük, jelezze az esetleges problémákat vagy visszajelzéseit.", + "EXECUTION": { + "TITLE": "Műveletek", + "DESCRIPTION": "A műveletek lehetővé teszik egyedi kód futtatását API-kérésekre, eseményekre vagy konkrét függvényekre válaszul. Használja őket a Zitadel kiterjesztéséhez, a munkafolyamatok automatizálásához és más rendszerekkel való integrációhoz.", + "TYPES": { + "request": "Kérés", + "response": "Válasz", + "event": "Események", + "function": "Függvény" + }, + "DIALOG": { + "CREATE_TITLE": "Művelet létrehozása", + "UPDATE_TITLE": "Művelet frissítése", + "TYPE": { + "DESCRIPTION": "Válassza ki, mikor szeretné futtatni ezt a műveletet", + "REQUEST": { + "TITLE": "Kérés", + "DESCRIPTION": "A Zitadelen belül előforduló kérések. Ez lehet például egy bejelentkezési kérés hívása." + }, + "RESPONSE": { + "TITLE": "Válasz", + "DESCRIPTION": "Válasz egy Zitadelen belüli kérésre. Gondoljon arra a válaszra, amelyet egy felhasználó lekérésekor kap." + }, + "EVENTS": { + "TITLE": "Események", + "DESCRIPTION": "A Zitadelen belül zajló események. Ez bármi lehet, például egy felhasználó fiók létrehozása, sikeres bejelentkezés stb." + }, + "FUNCTIONS": { + "TITLE": "Függvények", + "DESCRIPTION": "Függvények, amelyeket a Zitadelen belül hívhat. Ez bármi lehet, az e-mail küldésétől a felhasználó létrehozásáig." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Válassza ki, hogy ez a művelet vonatkozik-e minden kérésre, egy adott szolgáltatásra (pl. felhasználókezelés) vagy egyetlen kérésre (pl. felhasználó létrehozása).", + "ALL": { + "TITLE": "Összes", + "DESCRIPTION": "Válassza ezt, ha minden kérésnél futtatni szeretné a műveletet" + }, + "ALL_EVENTS": "Válaszd ezt, ha minden eseménynél futtatni szeretnéd a műveletet", + "SELECT_SERVICE": { + "TITLE": "Szolgáltatás kiválasztása", + "DESCRIPTION": "Válasszon egy Zitadel szolgáltatást a művelethez." + }, + "SELECT_METHOD": { + "TITLE": "Módszer kiválasztása", + "DESCRIPTION": "Ha csak egy adott kérésnél szeretne futtatni, válassza ki itt", + "NOTE": "Ha nem választ módszert, a művelet minden kérésnél futni fog a kiválasztott szolgáltatásban." + }, + "FUNCTIONNAME": { + "TITLE": "Függvénynév", + "DESCRIPTION": "Válassza ki a futtatni kívánt függvényt" + }, + "SELECT_GROUP": { + "TITLE": "Csoport beállítása", + "DESCRIPTION": "Ha csak események egy csoportján szeretne futtatni, állítsa be itt a csoportot" + }, + "SELECT_EVENT": { + "TITLE": "Esemény kiválasztása", + "DESCRIPTION": "Ha csak egy adott eseményen szeretne futtatni, adja meg itt" + } + }, + "TARGET": { + "DESCRIPTION": "Választhat, hogy futtat egy célt, vagy ugyanazokkal a feltételekkel futtatja, mint más célokat.", + "TARGET": { + "DESCRIPTION": "A cél, amelyet futtatni szeretne ehhez a művelethez" + }, + "CONDITIONS": { + "DESCRIPTION": "Végrehajtási feltételek" + } + } + }, + "TABLE": { + "CONDITION": "Feltétel", + "TYPE": "Típus", + "TARGET": "Cél", + "CREATIONDATE": "Létrehozás dátuma" + } + }, + "TARGET": { + "TITLE": "Célok", + "DESCRIPTION": "A cél annak a kódnak a célja, amelyet egy műveletből szeretne futtatni. Hozzon létre itt egy célt, és adja hozzá a műveleteihez.", + "CREATE": { + "TITLE": "Cél létrehozása", + "DESCRIPTION": "Hozza létre saját célját a Zitadelen kívül", + "NAME": "Név", + "NAME_DESCRIPTION": "Adjon a céljának egy világos, leíró nevet, hogy később könnyen azonosítható legyen", + "TYPE": "Típus", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Hívás", + "restAsync": "REST Aszinkron" + }, + "TYPES_DESCRIPTION": "Webhook, a hívás kezeli az állapotkódot, de a válasz lényegtelen\nCall, a hívás kezeli az állapotkódot és a választ\nAsync, a hívás sem az állapotkódot, sem a választ nem kezeli, de párhuzamosan hívható más célokkal", + "ENDPOINT": "Végpont", + "ENDPOINT_DESCRIPTION": "Adja meg azt a végpontot, ahol a kódja található. Győződjön meg arról, hogy elérhető számunkra!", + "TIMEOUT": "Időtúllépés", + "TIMEOUT_DESCRIPTION": "Állítsa be a maximális időt, amíg a céljának válaszolnia kell. Ha tovább tart, leállítjuk a kérést.", + "INTERRUPT_ON_ERROR": "Hiba esetén megszakítás", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Állítsa le az összes végrehajtást, ha a célok hibát adnak vissza", + "INTERRUPT_ON_ERROR_WARNING": "Figyelem: Az „Hiba esetén megszakítás” funkció leállítja a műveleteket hiba esetén, ami kizáráshoz vezethet. Tesztelje kikapcsolt állapotban a bejelentkezés/létrehozás blokkolásának elkerülése érdekében.", + "AWAIT_RESPONSE": "Válaszra várás", + "AWAIT_RESPONSE_DESCRIPTION": "Megvárjuk a választ, mielőtt bármi mást tennénk. Hasznos, ha több célt szeretne használni egyetlen művelethez" + }, + "TABLE": { + "NAME": "Név", + "ENDPOINT": "Végpont", + "CREATIONDATE": "Létrehozás dátuma", + "REORDER": "Újrarendelés" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Teljes irányítása van az egész példány felett, beleértve minden szervezetet", "IAM_OWNER_VIEWER": "Jogosultsága van az egész példány átnézésére, beleértve minden szervezetet", @@ -790,7 +929,10 @@ "PHONESECTION": "Telefonszámok", "PASSWORDSECTION": "Kezdeti jelszó", "ADDRESSANDPHONESECTION": "Telefonszám", - "INITMAILDESCRIPTION": "Ha mindkét opció ki van választva, nem kerül kiküldésre inicializáló e-mail. Ha csak az egyik opció van kiválasztva, egy e-mailt küldünk az adatok megadására / ellenőrzésére." + "INITMAILDESCRIPTION": "Ha mindkét opció ki van választva, nem kerül kiküldésre inicializáló e-mail. Ha csak az egyik opció van kiválasztva, egy e-mailt küldünk az adatok megadására / ellenőrzésére.", + "SETUPAUTHENTICATIONLATER": "Állítsa be később az autentikációt ehhez a felhasználóhoz.", + "INVITATION": "Küldjön meghívó e-mailt az autentikáció beállításához és az e-mail hitelesítéséhez.", + "INITIALPASSWORD": "Állítson be egy kezdeti jelszót a felhasználónak." }, "CODEDIALOG": { "TITLE": "Telefonszám ellenőrzése", @@ -1354,6 +1496,7 @@ "BRANDING": "Márkaépítés", "PRIVACYPOLICY": "Külső hivatkozások", "OIDC": "OIDC token élettartam és lejárat", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Titokgenerátor", "SECURITY": "Biztonsági beállítások", "EVENTS": "Események", @@ -1369,7 +1512,8 @@ "APPEARANCE": "Megjelenés", "OTHER": "Egyéb", "STORAGE": "Tárolás" - } + }, + "BETA": "BÉTA" }, "SETTING": { "LANGUAGES": { @@ -1399,7 +1543,8 @@ "sv": "Svéd", "id": "Indonéz", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1488,6 +1633,16 @@ "ACTIONS_DESCRIPTION": "A Műveletek v2 lehetővé teszik az adat futtatások és célok kezelését. Ha az opció engedélyezve van, használhatod az új API-t és annak funkcióit.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Egyedüli V1 Munkamenet Befejezése", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Ha a zászló engedélyezve van, képes leszel egyetlen munkamenetet megszüntetni a bejelentkezési UI-ben, ha egy id_tokent biztosítasz egy `sid` claim-mel mint id_token_hint az end_session végpontnál. Megjegyzendő, hogy jelenleg az összes munkamenet ugyanabból a felhasználói ügynökből (böngésző) leállításra kerül a bejelentkezési UI-ben. A Session API-val kezelt munkamenetek már lehetővé teszik egyes munkamenetek megszüntetését.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Parent Error", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Ha a zászló engedélyezve van, az OIDC szülő hiba naplózva lesz a konzolban.", + "DISABLEUSERTOKENEVENT": "Felhasználói token esemény letiltása", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Backchannel Logout engedélyezése", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "A Back-Channel Logout megvalósítja az OpenID Connect Back-Channel Logout 1.0-t, és használható az ügyfelek értesítésére a munkamenet befejezéséről az OpenID szolgáltatónál.", + "PERMISSIONCHECKV2": "Engedély ellenőrzés V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Ha a zászló engedélyezve van, használhatja az új API-t és annak funkcióit.", + "WEBKEY": "Webkulcs", + "WEBKEY_DESCRIPTION": "Ha a zászló engedélyezve van, használhatja az új API-t és annak funkcióit.", "STATES": { "INHERITED": "Örököl", "ENABLED": "Engedélyezve", @@ -1500,7 +1655,10 @@ }, "RESET": "Mindent állíts öröklésre", "CONSOLEUSEV2USERAPI": "Használja a V2 API-t a konzolban felhasználók létrehozásához", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Ha ez a jelző engedélyezve van, a konzol a V2 User API-t használja új felhasználók létrehozásához. A V2 API-val az újonnan létrehozott felhasználók kezdeti állapot nélkül indulnak." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Ha ez a jelző engedélyezve van, a konzol a V2 User API-t használja új felhasználók létrehozásához. A V2 API-val az újonnan létrehozott felhasználók kezdeti állapot nélkül indulnak.", + "LOGINV2": "Bejelentkezés V2", + "LOGINV2_DESCRIPTION": "Ennek engedélyezésével aktiválódik az új, TypeScript-alapú bejelentkezési felület, amely jobb biztonságot, teljesítményt és testreszabhatóságot nyújt.", + "LOGINV2_BASEURI": "Alap URI" }, "DIALOG": { "RESET": { @@ -1624,9 +1782,9 @@ "de": "Deutsch", "en": "English", "es": "Español", - "fr": "Francia", - "it": "Olasz", - "ja": "Japán", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", "pl": "Lengyel", "zh": "Egyszerűsített kínai", "bg": "Bolgár", @@ -1637,7 +1795,9 @@ "nl": "Holland", "sv": "Svéd", "id": "Indonéz", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "E-mail ellenőrzés kész", @@ -2595,12 +2755,12 @@ "3": "Egyéb" }, "LANGUAGES": { - "de": "Német", - "en": "Angol", - "es": "Spanyol", - "fr": "Francia", - "it": "Olasz", - "ja": "Japán", + "de": "Deutsch", + "en": "English", + "es": "Español", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", "pl": "Lengyel", "zh": "Egyszerűsített kínai", "bg": "Bolgár", @@ -2611,7 +2771,9 @@ "nl": "Holland", "sv": "Svéd", "id": "Indonéz", - "hu": "Magyar" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Hozzáadás egy menedzsert", diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json index 36c0658de5..ca788a9467 100644 --- a/console/src/assets/i18n/id.json +++ b/console/src/assets/i18n/id.json @@ -69,7 +69,8 @@ "FLOWS": { "TITLE": "Mengalir", "DESCRIPTION": "Pilih alur autentikasi dan picu tindakan Anda pada peristiwa tertentu dalam alur ini." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, versi baru dan lebih baik dari Actions, sekarang tersedia. Versi saat ini masih dapat diakses, tetapi pengembangan di masa depan akan difokuskan pada versi baru ini yang pada akhirnya akan menggantikan versi saat ini." }, "SETTINGS": { "INSTANCE": { @@ -173,6 +174,32 @@ "DESCRIPTION": "Masa pakai token penyegaran yang menganggur adalah waktu maksimum token penyegaran tidak dapat digunakan." } }, + "WEB_KEYS": { + "DESCRIPTION": "Kelola Kunci Web OIDC Anda untuk menandatangani dan memvalidasi token dengan aman untuk instance ZITADEL Anda.", + "TABLE": { + "TITLE": "Kunci Web Aktif dan Mendatang", + "DESCRIPTION": "Kunci web Anda yang aktif dan akan datang. Mengaktifkan kunci baru akan menonaktifkan kunci yang sedang digunakan.", + "NOTE": "Catatan: Endpoint JWKs OIDC mengembalikan respons yang dapat di-cache (default 5 menit). Hindari mengaktifkan kunci terlalu cepat, karena mungkin belum tersedia di cache dan klien.", + "ACTIVATE": "Aktifkan Kunci Web Berikutnya", + "ACTIVE": "Saat ini aktif", + "NEXT": "Berikutnya dalam antrean", + "FUTURE": "Mendatang", + "WARNING": "Kunci Web berusia kurang dari 5 menit" + }, + "CREATE": { + "TITLE": "Buat Kunci Web Baru", + "DESCRIPTION": "Membuat kunci web baru akan menambahkannya ke daftar Anda. ZITADEL secara default menggunakan kunci RSA2048 dengan fungsi hash SHA256.", + "KEY_TYPE": "Jenis Kunci", + "BITS": "Bit", + "HASHER": "Hasher", + "CURVE": "Kurva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Kunci Web Sebelumnya", + "DESCRIPTION": "Ini adalah kunci web sebelumnya yang tidak lagi aktif.", + "DEACTIVATED_ON": "Dinonaktifkan pada" + } + }, "MESSAGE_TEXTS": { "TITLE": "Teks Pesan", "DESCRIPTION": "Sesuaikan teks email notifikasi atau pesan SMS Anda. Jika Anda ingin menonaktifkan beberapa bahasa, batasi bahasa tersebut di pengaturan bahasa instance Anda.", @@ -469,6 +496,118 @@ "DOWNLOAD": "Unduh", "APPLY": "Menerapkan" }, + "ACTIONSTWO": { + "BETA_NOTE": "Anda saat ini menggunakan Actions V2 baru, yang masih dalam versi beta. Versi sebelumnya, Versi 1, masih tersedia tetapi akan dihentikan di masa depan. Silakan laporkan masalah atau berikan masukan.", + "EXECUTION": { + "TITLE": "Tindakan", + "DESCRIPTION": "Tindakan memungkinkan Anda menjalankan kode khusus sebagai respons terhadap permintaan API, peristiwa, atau fungsi tertentu. Gunakan ini untuk memperluas Zitadel, mengotomatiskan alur kerja, dan berintegrasi dengan sistem lain.", + "TYPES": { + "request": "Permintaan", + "response": "Respons", + "event": "Peristiwa", + "function": "Fungsi" + }, + "DIALOG": { + "CREATE_TITLE": "Buat Tindakan", + "UPDATE_TITLE": "Perbarui Tindakan", + "TYPE": { + "DESCRIPTION": "Pilih kapan Anda ingin Tindakan ini dijalankan", + "REQUEST": { + "TITLE": "Permintaan", + "DESCRIPTION": "Permintaan yang terjadi di dalam Zitadel. Ini bisa berupa sesuatu seperti panggilan permintaan login." + }, + "RESPONSE": { + "TITLE": "Respons", + "DESCRIPTION": "Respons dari permintaan di dalam Zitadel. Pikirkan respons yang Anda dapatkan kembali dari pengambilan pengguna." + }, + "EVENTS": { + "TITLE": "Peristiwa", + "DESCRIPTION": "Peristiwa yang terjadi di dalam Zitadel. Ini bisa berupa apa saja seperti pengguna membuat akun, login berhasil, dll." + }, + "FUNCTIONS": { + "TITLE": "Fungsi", + "DESCRIPTION": "Fungsi yang dapat Anda panggil di dalam Zitadel. Ini bisa berupa apa saja mulai dari mengirim email hingga membuat pengguna." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Pilih apakah tindakan ini berlaku untuk semua permintaan, layanan tertentu (mis. manajemen pengguna), atau permintaan tunggal (mis. buat pengguna).", + "ALL": { + "TITLE": "Semua", + "DESCRIPTION": "Pilih ini jika Anda ingin menjalankan tindakan Anda pada setiap permintaan" + }, + "ALL_EVENTS": "Pilih ini jika Anda ingin menjalankan aksi Anda pada setiap peristiwa", + "SELECT_SERVICE": { + "TITLE": "Pilih Layanan", + "DESCRIPTION": "Pilih Layanan Zitadel untuk tindakan Anda." + }, + "SELECT_METHOD": { + "TITLE": "Pilih Metode", + "DESCRIPTION": "Jika Anda hanya ingin menjalankan pada permintaan tertentu, pilih di sini", + "NOTE": "Jika Anda tidak memilih metode, tindakan Anda akan berjalan pada setiap permintaan di layanan yang Anda pilih." + }, + "FUNCTIONNAME": { + "TITLE": "Nama Fungsi", + "DESCRIPTION": "Pilih fungsi yang ingin Anda jalankan" + }, + "SELECT_GROUP": { + "TITLE": "Tetapkan Grup", + "DESCRIPTION": "Jika Anda hanya ingin menjalankan pada grup peristiwa, tetapkan grup di sini" + }, + "SELECT_EVENT": { + "TITLE": "Pilih Peristiwa", + "DESCRIPTION": "Jika Anda hanya ingin menjalankan pada peristiwa tertentu, tentukan di sini" + } + }, + "TARGET": { + "DESCRIPTION": "Anda dapat memilih untuk menjalankan target, atau menjalankannya dengan kondisi yang sama dengan target lain.", + "TARGET": { + "DESCRIPTION": "Target yang ingin Anda jalankan untuk tindakan ini" + }, + "CONDITIONS": { + "DESCRIPTION": "Kondisi Eksekusi" + } + } + }, + "TABLE": { + "CONDITION": "Kondisi", + "TYPE": "Jenis", + "TARGET": "Target", + "CREATIONDATE": "Tanggal Pembuatan" + } + }, + "TARGET": { + "TITLE": "Target", + "DESCRIPTION": "Target adalah tujuan kode yang ingin Anda jalankan dari tindakan. Buat target di sini dan tambahkan ke tindakan Anda.", + "CREATE": { + "TITLE": "Buat Target Anda", + "DESCRIPTION": "Buat target Anda sendiri di luar Zitadel", + "NAME": "Nama", + "NAME_DESCRIPTION": "Beri target Anda nama yang jelas dan deskriptif agar mudah diidentifikasi nanti", + "TYPE": "Jenis", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Panggilan REST", + "restAsync": "REST Asinkron" + }, + "TYPES_DESCRIPTION": "Webhook, panggilan menangani kode status tetapi respons tidak relevan\nCall, panggilan menangani kode status dan respons\nAsync, panggilan tidak menangani kode status maupun respons, tetapi dapat dipanggil secara paralel dengan Target lain", + "ENDPOINT": "Titik Akhir", + "ENDPOINT_DESCRIPTION": "Masukkan titik akhir tempat kode Anda dihosting. Pastikan dapat diakses oleh kami!", + "TIMEOUT": "Batas Waktu", + "TIMEOUT_DESCRIPTION": "Tetapkan waktu maksimum target Anda untuk merespons. Jika membutuhkan waktu lebih lama, kami akan menghentikan permintaan.", + "INTERRUPT_ON_ERROR": "Interupsi jika Terjadi Kesalahan", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Hentikan semua eksekusi saat target mengembalikan kesalahan", + "INTERRUPT_ON_ERROR_WARNING": "Perhatian: \"Interupsi jika Terjadi Kesalahan\" akan menghentikan operasi jika terjadi kegagalan, berisiko mengunci akses. Uji dengan opsi ini dinonaktifkan untuk mencegah pemblokiran login/pembuatan.", + "AWAIT_RESPONSE": "Tunggu Respons", + "AWAIT_RESPONSE_DESCRIPTION": "Kami akan menunggu respons sebelum melakukan hal lain. Berguna jika Anda berniat menggunakan beberapa target untuk satu tindakan" + }, + "TABLE": { + "NAME": "Nama", + "ENDPOINT": "Titik Akhir", + "CREATIONDATE": "Tanggal Pembuatan", + "REORDER": "Susun ulang" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Memiliki kendali atas seluruh instansi, termasuk semua organisasi", "IAM_OWNER_VIEWER": "Memiliki izin untuk meninjau seluruh instansi, termasuk semua organisasi", @@ -730,7 +869,10 @@ "PHONESECTION": "Nomor telepon", "PASSWORDSECTION": "Kata Sandi Awal", "ADDRESSANDPHONESECTION": "Nomor telepon", - "INITMAILDESCRIPTION": "Jika kedua opsi dipilih, tidak ada email untuk inisialisasi yang akan dikirim. Jika hanya salah satu opsi yang dipilih, email untuk menyediakan/memverifikasi data akan dikirim." + "INITMAILDESCRIPTION": "Jika kedua opsi dipilih, tidak ada email untuk inisialisasi yang akan dikirim. Jika hanya salah satu opsi yang dipilih, email untuk menyediakan/memverifikasi data akan dikirim.", + "SETUPAUTHENTICATIONLATER": "Atur autentikasi nanti untuk pengguna ini.", + "INVITATION": "Kirim Email undangan untuk pengaturan autentikasi dan verifikasi Email.", + "INITIALPASSWORD": "Tetapkan kata sandi awal untuk pengguna." }, "CODEDIALOG": { "TITLE": "Verifikasi Nomor Telepon", @@ -1232,6 +1374,7 @@ "BRANDING": "merek", "PRIVACYPOLICY": "Tautan eksternal", "OIDC": "Masa berlaku dan masa berlaku Token OIDC", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Pembuat Rahasia", "SECURITY": "Pengaturan keamanan", "EVENTS": "Acara", @@ -1247,7 +1390,8 @@ "APPEARANCE": "Penampilan", "OTHER": "Lainnya", "STORAGE": "Penyimpanan" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { @@ -1277,7 +1421,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1362,6 +1507,15 @@ "USERSCHEMA_DESCRIPTION": "Skema Pengguna memungkinkan untuk mengelola skema data pengguna. Jika tanda ini diaktifkan, Anda akan dapat menggunakan API baru dan fitur-fiturnya.", "ACTIONS": "Tindakan", "ACTIONS_DESCRIPTION": "Tindakan v2 memungkinkan untuk mengelola eksekusi dan target data. Jika tanda ini diaktifkan, Anda akan dapat menggunakan API baru dan fitur-fiturnya.", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "If the flag is enabled, the OIDC parent error will be logged in the console.", + "DISABLEUSERTOKENEVENT": "Disable User Token Event", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Enable Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "The Back-Channel Logout implements OpenID Connect Back-Channel Logout 1.0 and can be used to notify clients about session termination at the OpenID Provider.", + "PERMISSIONCHECKV2": "Permission Check V2", + "PERMISSIONCHECKV2_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", + "WEBKEY": "Web Key", + "WEBKEY_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", "STATES": { "INHERITED": "Mewarisi", "ENABLED": "Diaktifkan", "DISABLED": "Dengan disabilitas" }, "INHERITED_DESCRIPTION": "Ini menetapkan nilai ke nilai default sistem.", "INHERITEDINDICATOR_DESCRIPTION": { @@ -1370,7 +1524,10 @@ }, "RESET": "Tetapkan semua untuk diwarisi", "CONSOLEUSEV2USERAPI": "Gunakan API V2 di konsol untuk pembuatan pengguna", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Ketika flag ini diaktifkan, konsol menggunakan API Pengguna V2 untuk membuat pengguna baru. Dengan API V2, pengguna yang baru dibuat dimulai tanpa keadaan awal." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Ketika flag ini diaktifkan, konsol menggunakan API Pengguna V2 untuk membuat pengguna baru. Dengan API V2, pengguna yang baru dibuat dimulai tanpa keadaan awal.", + "LOGINV2": "Login V2", + "LOGINV2_DESCRIPTION": "Mengaktifkan ini akan mengaktifkan antarmuka login baru berbasis TypeScript dengan keamanan, performa, dan kustomisasi yang lebih baik.", + "LOGINV2_BASEURI": "URI dasar" }, "DIALOG": { "RESET": { @@ -1504,7 +1661,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Verifikasi email selesai", @@ -2297,7 +2456,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Tambahkan Manajer", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index b401cbcfb8..60266bdac5 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flussi", "DESCRIPTION": "Scegli un flusso di autenticazione e attiva la tua azione su un evento specifico all'interno di questo flusso." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, una nuova versione migliorata di Actions, è ora disponibile. La versione attuale è ancora accessibile, ma i futuri sviluppi si concentreranno su quella nuova, che alla fine sostituirà la versione corrente." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "La durata massima di un token di refresh inattivo è il tempo massimo in cui un token di refresh può rimanere inutilizzato." } }, + "WEB_KEYS": { + "DESCRIPTION": "Gestisci le tue chiavi Web OIDC per firmare e convalidare in modo sicuro i token per la tua istanza di ZITADEL.", + "TABLE": { + "TITLE": "Chiavi Web Attive e Future", + "DESCRIPTION": "Le tue chiavi web attive e future. L'attivazione di una nuova chiave disattiverà quella attuale.", + "NOTE": "Nota: L'endpoint JWKs OIDC restituisce una risposta memorizzabile nella cache (predefinito 5 min). Evita di attivare una chiave troppo presto, poiché potrebbe non essere ancora disponibile nelle cache e nei client.", + "ACTIVATE": "Attiva la prossima Chiave Web", + "ACTIVE": "Attualmente attiva", + "NEXT": "Prossima in coda", + "FUTURE": "Futura", + "WARNING": "La chiave web ha meno di 5 minuti" + }, + "CREATE": { + "TITLE": "Crea una nuova Chiave Web", + "DESCRIPTION": "Creare una nuova chiave web la aggiungerà alla tua lista. ZITADEL utilizza chiavi RSA2048 con hash SHA256 per impostazione predefinita.", + "KEY_TYPE": "Tipo di Chiave", + "BITS": "Bit", + "HASHER": "Hasher", + "CURVE": "Curva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Chiavi Web Precedenti", + "DESCRIPTION": "Queste sono le tue chiavi web precedenti che non sono più attive.", + "DEACTIVATED_ON": "Disattivata il" + } + }, "MESSAGE_TEXTS": { "TITLE": "Testi dei Messaggi", "DESCRIPTION": "Personalizza i testi delle tue email di notifica o messaggi SMS. Se vuoi disabilitare alcune lingue, limitale nelle impostazioni lingua delle tue istanze.", @@ -501,6 +528,118 @@ "DOWNLOAD": "Scarica", "APPLY": "Applicare" }, + "ACTIONSTWO": { + "BETA_NOTE": "Stai attualmente utilizzando la nuova versione Actions V2, che è in beta. La precedente Versione 1 è ancora disponibile, ma sarà dismessa in futuro. Ti preghiamo di segnalare eventuali problemi o feedback.", + "EXECUTION": { + "TITLE": "Azioni", + "DESCRIPTION": "Le azioni consentono di eseguire codice personalizzato in risposta a richieste API, eventi o funzioni specifiche. Usale per estendere Zitadel, automatizzare i flussi di lavoro e integrarti con altri sistemi.", + "TYPES": { + "request": "Richiesta", + "response": "Risposta", + "event": "Eventi", + "function": "Funzione" + }, + "DIALOG": { + "CREATE_TITLE": "Crea un'azione", + "UPDATE_TITLE": "Aggiorna un'azione", + "TYPE": { + "DESCRIPTION": "Seleziona quando vuoi che venga eseguita questa azione", + "REQUEST": { + "TITLE": "Richiesta", + "DESCRIPTION": "Richieste che si verificano all'interno di Zitadel. Potrebbe trattarsi di una chiamata di richiesta di accesso." + }, + "RESPONSE": { + "TITLE": "Risposta", + "DESCRIPTION": "Una risposta a una richiesta all'interno di Zitadel. Pensa alla risposta che ricevi quando recuperi un utente." + }, + "EVENTS": { + "TITLE": "Eventi", + "DESCRIPTION": "Eventi che si verificano all'interno di Zitadel. Potrebbe trattarsi di qualsiasi cosa, come un utente che crea un account, un accesso riuscito, ecc." + }, + "FUNCTIONS": { + "TITLE": "Funzioni", + "DESCRIPTION": "Funzioni che puoi chiamare all'interno di Zitadel. Potrebbe trattarsi di qualsiasi cosa, dall'invio di un'e-mail alla creazione di un utente." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Scegli se questa azione si applica a tutte le richieste, a un servizio specifico (ad es. gestione utenti) o a una singola richiesta (ad es. crea utente).", + "ALL": { + "TITLE": "Tutte", + "DESCRIPTION": "Seleziona questa opzione se vuoi eseguire la tua azione su ogni richiesta" + }, + "ALL_EVENTS": "Seleziona questo se vuoi eseguire la tua azione a ogni evento", + "SELECT_SERVICE": { + "TITLE": "Seleziona servizio", + "DESCRIPTION": "Scegli un servizio Zitadel per la tua azione." + }, + "SELECT_METHOD": { + "TITLE": "Seleziona metodo", + "DESCRIPTION": "Se vuoi eseguire solo su una richiesta specifica, selezionala qui", + "NOTE": "Se non selezioni un metodo, la tua azione verrà eseguita su ogni richiesta nel servizio selezionato." + }, + "FUNCTIONNAME": { + "TITLE": "Nome funzione", + "DESCRIPTION": "Scegli la funzione che vuoi eseguire" + }, + "SELECT_GROUP": { + "TITLE": "Imposta gruppo", + "DESCRIPTION": "Se vuoi eseguire solo su un gruppo di eventi, imposta il gruppo qui" + }, + "SELECT_EVENT": { + "TITLE": "Seleziona evento", + "DESCRIPTION": "Se vuoi eseguire solo su un evento specifico, specificalo qui" + } + }, + "TARGET": { + "DESCRIPTION": "Puoi scegliere di eseguire un obiettivo o di eseguirlo alle stesse condizioni di altri obiettivi.", + "TARGET": { + "DESCRIPTION": "L'obiettivo che vuoi eseguire per questa azione" + }, + "CONDITIONS": { + "DESCRIPTION": "Condizioni di esecuzione" + } + } + }, + "TABLE": { + "CONDITION": "Condizione", + "TYPE": "Tipo", + "TARGET": "Obiettivo", + "CREATIONDATE": "Data di creazione" + } + }, + "TARGET": { + "TITLE": "Obiettivi", + "DESCRIPTION": "Un obiettivo è la destinazione del codice che vuoi eseguire da un'azione. Crea un obiettivo qui e aggiungilo alle tue azioni.", + "CREATE": { + "TITLE": "Crea il tuo obiettivo", + "DESCRIPTION": "Crea il tuo obiettivo al di fuori di Zitadel", + "NAME": "Nome", + "NAME_DESCRIPTION": "Dai al tuo obiettivo un nome chiaro e descrittivo per renderlo facile da identificare in seguito", + "TYPE": "Tipo", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Chiamata REST", + "restAsync": "REST Asincrono" + }, + "TYPES_DESCRIPTION": "Webhook, la chiamata gestisce il codice di stato ma la risposta è irrilevante\nCall, la chiamata gestisce il codice di stato e la risposta\nAsync, la chiamata non gestisce né il codice di stato né la risposta, ma può essere eseguita in parallelo con altri obiettivi", + "ENDPOINT": "Endpoint", + "ENDPOINT_DESCRIPTION": "Inserisci l'endpoint in cui è ospitato il tuo codice. Assicurati che sia accessibile per noi!", + "TIMEOUT": "Timeout", + "TIMEOUT_DESCRIPTION": "Imposta il tempo massimo che il tuo obiettivo ha per rispondere. Se impiega più tempo, interromperemo la richiesta.", + "INTERRUPT_ON_ERROR": "Interrompi in caso di errore", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Interrompi tutte le esecuzioni quando gli obiettivi restituiscono un errore", + "INTERRUPT_ON_ERROR_WARNING": "Attenzione: “Interrompi in caso di errore” arresta le operazioni in caso di fallimento, rischiando il blocco. Testare con l’opzione disattivata per evitare il blocco dell’accesso/creazione.", + "AWAIT_RESPONSE": "Attendi risposta", + "AWAIT_RESPONSE_DESCRIPTION": "Aspetteremo una risposta prima di fare altro. Utile se intendi utilizzare più obiettivi per una singola azione" + }, + "TABLE": { + "NAME": "Nome", + "ENDPOINT": "Endpoint", + "CREATIONDATE": "Data di creazione", + "REORDER": "Riordina" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Ha il controllo sull'intera istanza, comprese tutte le organizzazioni", "IAM_OWNER_VIEWER": "Ha l'autorizzazione per esaminare l'intera istanza, comprese tutte le organizzazioni", @@ -789,7 +928,10 @@ "PHONESECTION": "Phone numbers", "PASSWORDSECTION": "Password iniziale", "ADDRESSANDPHONESECTION": "Numero di telefono", - "INITMAILDESCRIPTION": "Se vengono selezionate entrambe le opzioni, non verrà inviata alcuna e-mail per l'inizializzazione. Se solo una delle opzioni viene selezionata, verrà inviata una mail per fornire/verificare i dati." + "INITMAILDESCRIPTION": "Se vengono selezionate entrambe le opzioni, non verrà inviata alcuna e-mail per l'inizializzazione. Se solo una delle opzioni viene selezionata, verrà inviata una mail per fornire/verificare i dati.", + "SETUPAUTHENTICATIONLATER": "Configura l'autenticazione più tardi per questo utente.", + "INVITATION": "Invia un'e-mail di invito per la configurazione dell'autenticazione e la verifica dell'e-mail.", + "INITIALPASSWORD": "Imposta una password iniziale per l'utente." }, "CODEDIALOG": { "TITLE": "Verificare il numero di telefono", @@ -1354,6 +1496,7 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Informativa sulla privacy e TOS", "OIDC": "OIDC Token lifetime e scadenza", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Aspetto dei segreti", "SECURITY": "Impostazioni di sicurezza", "EVENTS": "Eventi", @@ -1369,7 +1512,8 @@ "APPEARANCE": "Aspetto", "OTHER": "Altro", "STORAGE": "Dati" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { @@ -1399,7 +1543,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1490,6 +1635,16 @@ "ACTIONS_DESCRIPTION": "Le azioni v2 consentono di gestire le esecuzioni e gli obiettivi dei dati. Se l'indicatore è abilitato, potrai utilizzare la nuova API e le sue funzionalità.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Terminazione sessione", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Se il flag è abilitato, sarai in grado di terminare una singola sessione dall'interfaccia utente di accesso fornendo un id_token con una richiesta `sid` come id_token_hint nel punto finale di end_session. Tieni presente che attualmente tutte le sessioni dello stesso agente utente (browser) vengono terminate nell'interfaccia utente di accesso. Le sessioni gestite tramite l'API di sessione consentono già la terminazione di singole sessioni.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Parent Error", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Se il flag è abilitato, l'errore del genitore OIDC verrà registrato nella console.", + "DISABLEUSERTOKENEVENT": "Disabilita evento token utente", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Abilita Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Il Back-Channel Logout implementa OpenID Connect Back-Channel Logout 1.0 e può essere utilizzato per notificare ai client la terminazione della sessione presso il provider OpenID.", + "PERMISSIONCHECKV2": "Controllo permessi V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Se il flag è abilitato, potrai utilizzare la nuova API e le sue funzionalità.", + "WEBKEY": "Chiave Web", + "WEBKEY_DESCRIPTION": "Se il flag è abilitato, potrai utilizzare la nuova API e le sue funzionalità.", "STATES": { "INHERITED": "Predefinito", "ENABLED": "Abilitato", @@ -1502,7 +1657,10 @@ }, "RESET": "Imposta tutto su predefinito", "CONSOLEUSEV2USERAPI": "Utilizza l'API V2 nella console per la creazione degli utenti", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Quando questa opzione è abilitata, la console utilizza l'API V2 User per creare nuovi utenti. Con l'API V2, i nuovi utenti creati iniziano senza uno stato iniziale." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Quando questa opzione è abilitata, la console utilizza l'API V2 User per creare nuovi utenti. Con l'API V2, i nuovi utenti creati iniziano senza uno stato iniziale.", + "LOGINV2": "Accesso V2", + "LOGINV2_DESCRIPTION": "Abilitando questa opzione si attiva la nuova interfaccia di login basata su TypeScript con sicurezza, prestazioni e personalizzazione migliorate.", + "LOGINV2_BASEURI": "URI di base" }, "DIALOG": { "RESET": { @@ -1639,7 +1797,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Verifica dell'e-mail terminata con successo.", @@ -2589,7 +2749,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Aggiungi un manager", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 3d57150573..288d491ce7 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "フロー", "DESCRIPTION": "認証フローを選択し、そのフロー内の特定のイベントでアクションをトリガーします。" - } + }, + "ACTIONSTWO_NOTE": "Actions V2(アクションズV2)、改善された新しいバージョンが利用可能になりました。現在のバージョンも引き続き利用可能ですが、今後の開発は新バージョンに集中し、最終的には現在のバージョンを置き換える予定です。" }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "アイドル状態のリフレッシュトークンの有効期間は、リフレッシュトークンが使用されない最大時間です。" } }, + "WEB_KEYS": { + "DESCRIPTION": "ZITADELインスタンスのトークンを安全に署名および検証するために、OIDC Webキーを管理します。", + "TABLE": { + "TITLE": "アクティブおよび今後のWebキー", + "DESCRIPTION": "現在アクティブなWebキーと、今後使用予定のWebキーです。新しいキーをアクティブ化すると、現在のキーは無効になります。", + "NOTE": "注意: JWKs OIDCエンドポイントはキャッシュ可能なレスポンスを返します(デフォルト5分)。キーを早くアクティブ化しすぎると、キャッシュやクライアントでまだ利用できない可能性があります。", + "ACTIVATE": "次のWebキーをアクティブ化", + "ACTIVE": "現在アクティブ", + "NEXT": "次のキュー", + "FUTURE": "今後", + "WARNING": "ウェブキーは5分未満です。" + }, + "CREATE": { + "TITLE": "新しいWebキーを作成", + "DESCRIPTION": "新しいWebキーを作成すると、リストに追加されます。ZITADELはデフォルトでRSA2048キーとSHA256ハッシュを使用します。", + "KEY_TYPE": "キーの種類", + "BITS": "ビット", + "HASHER": "ハッシュ方式", + "CURVE": "カーブ" + }, + "PREVIOUS_TABLE": { + "TITLE": "以前のWebキー", + "DESCRIPTION": "これらは、すでに無効になった以前のWebキーです。", + "DEACTIVATED_ON": "無効化日" + } + }, "MESSAGE_TEXTS": { "TITLE": "メッセージテキスト", "DESCRIPTION": "通知メールやSMSメッセージのテキストをカスタマイズします。一部の言語を無効にしたい場合は、インスタンスの言語設定で制限してください。", @@ -502,6 +529,118 @@ "DOWNLOAD": "ダウンロード", "APPLY": "アプライ" }, + "ACTIONSTWO": { + "BETA_NOTE": "現在、新しいActions V2(ベータ版)を使用しています。以前のバージョン1はまだ利用可能ですが、今後廃止される予定です。問題やフィードバックがあればお知らせください。", + "EXECUTION": { + "TITLE": "アクション", + "DESCRIPTION": "アクションを使用すると、APIリクエスト、イベント、または特定の関数に応答してカスタムコードを実行できます。これらを使用して、Zitadelを拡張し、ワークフローを自動化し、他のシステムと統合します。", + "TYPES": { + "request": "リクエスト", + "response": "レスポンス", + "event": "イベント", + "function": "関数" + }, + "DIALOG": { + "CREATE_TITLE": "アクションを作成", + "UPDATE_TITLE": "アクションを更新", + "TYPE": { + "DESCRIPTION": "このアクションを実行するタイミングを選択します", + "REQUEST": { + "TITLE": "リクエスト", + "DESCRIPTION": "Zitadel内で発生するリクエスト。これはログインリクエストの呼び出しのようなものです。" + }, + "RESPONSE": { + "TITLE": "レスポンス", + "DESCRIPTION": "Zitadel内のリクエストからのレスポンス。ユーザーのフェッチから返されるレスポンスを考えてください。" + }, + "EVENTS": { + "TITLE": "イベント", + "DESCRIPTION": "Zitadel内で発生するイベント。これは、ユーザーアカウントの作成、ログインの成功など、あらゆる可能性があります。" + }, + "FUNCTIONS": { + "TITLE": "関数", + "DESCRIPTION": "Zitadel内で呼び出すことができる関数。これは、電子メールの送信からユーザーの作成まで、あらゆる可能性があります。" + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "このアクションをすべてのリクエスト、特定のサービス(例:ユーザー管理)、または単一のリクエスト(例:ユーザーの作成)に適用するかどうかを選択します。", + "ALL": { + "TITLE": "すべて", + "DESCRIPTION": "すべてのリクエストでアクションを実行する場合は、これを選択します" + }, + "ALL_EVENTS": "すべてのイベントでアクションを実行する場合はこれを選択してください", + "SELECT_SERVICE": { + "TITLE": "サービスを選択", + "DESCRIPTION": "アクションのZitadelサービスを選択します。" + }, + "SELECT_METHOD": { + "TITLE": "メソッドを選択", + "DESCRIPTION": "特定のリクエストでのみ実行する場合は、ここで選択します", + "NOTE": "メソッドを選択しない場合、アクションは選択したサービスのすべてのリクエストで実行されます。" + }, + "FUNCTIONNAME": { + "TITLE": "関数名", + "DESCRIPTION": "実行する関数を選択します" + }, + "SELECT_GROUP": { + "TITLE": "グループを設定", + "DESCRIPTION": "イベントのグループでのみ実行する場合は、ここでグループを設定します" + }, + "SELECT_EVENT": { + "TITLE": "イベントを選択", + "DESCRIPTION": "特定のイベントでのみ実行する場合は、ここで指定します" + } + }, + "TARGET": { + "DESCRIPTION": "ターゲットを実行するか、他のターゲットと同じ条件で実行するかを選択できます。", + "TARGET": { + "DESCRIPTION": "このアクションで実行するターゲット" + }, + "CONDITIONS": { + "DESCRIPTION": "実行条件" + } + } + }, + "TABLE": { + "CONDITION": "条件", + "TYPE": "タイプ", + "TARGET": "ターゲット", + "CREATIONDATE": "作成日" + } + }, + "TARGET": { + "TITLE": "ターゲット", + "DESCRIPTION": "ターゲットは、アクションから実行するコードの宛先です。ここでターゲットを作成し、アクションに追加します。", + "CREATE": { + "TITLE": "ターゲットを作成", + "DESCRIPTION": "Zitadelの外部で独自のターゲットを作成します", + "NAME": "名前", + "NAME_DESCRIPTION": "後で簡単に識別できるように、ターゲットに明確でわかりやすい名前を付けます", + "TYPE": "タイプ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST 呼び出し", + "restAsync": "REST 非同期" + }, + "TYPES_DESCRIPTION": "Webhook、呼び出しはステータスコードを処理しますが、応答は無関係です\nCall、呼び出しはステータスコードと応答を処理します\nAsync、呼び出しはステータスコードも応答も処理しませんが、他のターゲットと並行して呼び出すことができます", + "ENDPOINT": "エンドポイント", + "ENDPOINT_DESCRIPTION": "コードがホストされているエンドポイントを入力します。アクセス可能であることを確認してください。", + "TIMEOUT": "タイムアウト", + "TIMEOUT_DESCRIPTION": "ターゲットが応答する最大時間を設定します。これより時間がかかる場合は、リクエストを停止します。", + "INTERRUPT_ON_ERROR": "エラー時に中断", + "INTERRUPT_ON_ERROR_DESCRIPTION": "ターゲットがエラーを返した場合、すべての実行を停止します", + "INTERRUPT_ON_ERROR_WARNING": "注意:「エラー時に中断」を有効にすると、失敗時に処理が停止し、ロックアウトのリスクがあります。ログイン/作成のブロックを防ぐため、無効にしてテストしてください。", + "AWAIT_RESPONSE": "レスポンスを待機", + "AWAIT_RESPONSE_DESCRIPTION": "他の処理を行う前にレスポンスを待機します。単一のアクションに複数のターゲットを使用する場合に便利です" + }, + "TABLE": { + "NAME": "名前", + "ENDPOINT": "エンドポイント", + "CREATIONDATE": "作成日", + "REORDER": "順序を変更" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "すべての組織を含むインスタンス全体を管理する権限を持ちます", "IAM_OWNER_VIEWER": "すべての組織を含むインスタンス全体を閲覧する権限を持ちます", @@ -790,7 +929,10 @@ "PHONESECTION": "電話番号", "PASSWORDSECTION": "初期パスワード", "ADDRESSANDPHONESECTION": "電話番号", - "INITMAILDESCRIPTION": "両方のオプションが選択されている場合、初期セットアップ用のメールは送信されません。オプションのいずれかが選択されている場合、データを提供・認証するためのメールが送信されます。" + "INITMAILDESCRIPTION": "両方のオプションが選択されている場合、初期セットアップ用のメールは送信されません。オプションのいずれかが選択されている場合、データを提供・認証するためのメールが送信されます。", + "SETUPAUTHENTICATIONLATER": "このユーザーの認証を後で設定します。", + "INVITATION": "認証設定とメール確認のための招待メールを送信してください。", + "INITIALPASSWORD": "ユーザーの初期パスワードを設定してください。" }, "CODEDIALOG": { "TITLE": "電話番号の検証", @@ -1354,6 +1496,7 @@ "BRANDING": "ブランディング", "PRIVACYPOLICY": "プライバシーポリシー", "OIDC": "OIDCトークンのライフタイムと有効期限", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "シークレット設定", "SECURITY": "セキュリティ設定", "EVENTS": "イベント", @@ -1369,7 +1512,8 @@ "APPEARANCE": "設定", "OTHER": "その他", "STORAGE": "ストレージ" - } + }, + "BETA": "ベータ" }, "SETTING": { "LANGUAGES": { @@ -1399,7 +1543,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1490,6 +1635,16 @@ "ACTIONS_DESCRIPTION": "Actions v2は、データの実行とターゲットを管理できます。フラグが有効になっている場合、新しい APIとその機能を使用できます。", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 セッション終了", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "フラグが有効になっている場合、id_token を `sid` クレームと共に id_token_hint として end_session エンドポイントに提供することで、ログイン UI から単一のセッションを終了できるようになります。 現在、同じユーザー エージェント (ブラウザ) からのすべてのセッションがログイン UI で終了することに注意してください。 セッション API を通じて管理されるセッションは、すでに単一のセッションの終了を許可しています。", + "DEBUGOIDCPARENTERROR": "デバッグ OIDC 親エラー", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "フラグが有効になっている場合、OIDC 親エラーはコンソールに記録されます。", + "DISABLEUSERTOKENEVENT": "ユーザートークンイベントを無効にする", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "バックチャネルログアウトを有効にする", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "バックチャネルログアウトは OpenID Connect バックチャネルログアウト 1.0 を実装し、OpenID プロバイダーでのセッション終了についてクライアントに通知するために使用できます。", + "PERMISSIONCHECKV2": "権限チェック V2", + "PERMISSIONCHECKV2_DESCRIPTION": "フラグが有効になっている場合、新しい API とその機能を使用できます。", + "WEBKEY": "ウェブキー", + "WEBKEY_DESCRIPTION": "フラグが有効になっている場合、新しい API とその機能を使用できます。", "STATES": { "INHERITED": "継承", "ENABLED": "有効", @@ -1502,7 +1657,10 @@ }, "RESET": "すべて継承に設定", "CONSOLEUSEV2USERAPI": "コンソールでユーザー作成のためにV2 APIを使用してください。", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "このフラグが有効化されると、コンソールはV2ユーザーAPIを使用して新しいユーザーを作成します。V2 APIでは、新しく作成されたユーザーは初期状態なしで開始します。" + "CONSOLEUSEV2USERAPI_DESCRIPTION": "このフラグが有効化されると、コンソールはV2ユーザーAPIを使用して新しいユーザーを作成します。V2 APIでは、新しく作成されたユーザーは初期状態なしで開始します。", + "LOGINV2": "ログイン V2", + "LOGINV2_DESCRIPTION": "これを有効にすると、セキュリティ、パフォーマンス、およびカスタマイズ性が向上した、TypeScript ベースの新しいログイン UI が有効になります。", + "LOGINV2_BASEURI": "ベースURI" }, "DIALOG": { "RESET": { @@ -1639,7 +1797,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "メール認証が完了しました", @@ -2613,7 +2773,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "マネージャーを追加する", diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json index 9de71f371f..437c43a3a1 100644 --- a/console/src/assets/i18n/ko.json +++ b/console/src/assets/i18n/ko.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "플로우", "DESCRIPTION": "인증 플로우를 선택하고 이 플로우 내의 특정 이벤트에서 작업을 트리거하세요." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, 개선된 새로운 버전이 출시되었습니다. 현재 버전은 여전히 접근할 수 있지만, 앞으로의 개발은 새로운 버전에 집중될 것이며, 결국 현재 버전을 대체할 것입니다." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "유휴 갱신 토큰 수명은 갱신 토큰이 사용되지 않는 최대 기간을 의미합니다." } }, + "WEB_KEYS": { + "DESCRIPTION": "ZITADEL 인스턴스의 토큰을 안전하게 서명하고 검증하기 위해 OIDC 웹 키를 관리하세요.", + "TABLE": { + "TITLE": "활성 및 예정된 웹 키", + "DESCRIPTION": "현재 활성화된 웹 키와 앞으로 활성화될 웹 키입니다. 새로운 키를 활성화하면 기존 키는 비활성화됩니다.", + "NOTE": "참고: JWKs OIDC 엔드포인트는 캐시 가능한 응답을 반환합니다 (기본값: 5분). 키를 너무 빨리 활성화하면 캐시 및 클라이언트에서 아직 사용할 수 없을 수 있습니다.", + "ACTIVATE": "다음 웹 키 활성화", + "ACTIVE": "현재 활성화됨", + "NEXT": "대기 중인 다음 키", + "FUTURE": "향후 사용 예정", + "WARNING": "웹 키가 5분 미만입니다." + }, + "CREATE": { + "TITLE": "새 웹 키 생성", + "DESCRIPTION": "새 웹 키를 생성하면 목록에 추가됩니다. ZITADEL은 기본적으로 RSA2048 키와 SHA256 해시 알고리즘을 사용합니다.", + "KEY_TYPE": "키 유형", + "BITS": "비트", + "HASHER": "해시 알고리즘", + "CURVE": "곡선" + }, + "PREVIOUS_TABLE": { + "TITLE": "이전 웹 키", + "DESCRIPTION": "더 이상 활성 상태가 아닌 이전 웹 키 목록입니다.", + "DEACTIVATED_ON": "비활성화된 날짜" + } + }, "MESSAGE_TEXTS": { "TITLE": "메시지 텍스트", "DESCRIPTION": "알림 이메일 또는 SMS 메시지의 텍스트를 사용자 정의하세요. 언어를 비활성화하려면 인스턴스의 언어 설정에서 제한하세요.", @@ -502,6 +529,118 @@ "DOWNLOAD": "다운로드", "APPLY": "적용" }, + "ACTIONSTWO": { + "BETA_NOTE": "현재 베타 버전인 새로운 Actions V2를 사용하고 있습니다. 이전 버전 1은 여전히 사용 가능하지만, 향후 중단될 예정입니다. 문제나 피드백이 있으면 알려주세요.", + "EXECUTION": { + "TITLE": "작업", + "DESCRIPTION": "작업을 통해 API 요청, 이벤트 또는 특정 함수에 대한 응답으로 사용자 지정 코드를 실행할 수 있습니다. 이를 사용하여 Zitadel을 확장하고 워크플로를 자동화하며 다른 시스템과 통합합니다.", + "TYPES": { + "request": "요청", + "response": "응답", + "event": "이벤트", + "function": "함수" + }, + "DIALOG": { + "CREATE_TITLE": "작업 생성", + "UPDATE_TITLE": "작업 업데이트", + "TYPE": { + "DESCRIPTION": "이 작업을 실행할 시점을 선택하십시오.", + "REQUEST": { + "TITLE": "요청", + "DESCRIPTION": "Zitadel 내에서 발생하는 요청. 이는 로그인 요청 호출과 같은 것일 수 있습니다." + }, + "RESPONSE": { + "TITLE": "응답", + "DESCRIPTION": "Zitadel 내 요청으로부터의 응답. 사용자를 가져올 때 받는 응답을 생각해 보십시오." + }, + "EVENTS": { + "TITLE": "이벤트", + "DESCRIPTION": "Zitadel 내에서 발생하는 이벤트. 이는 사용자 계정 생성, 로그인 성공 등 모든 것이 될 수 있습니다." + }, + "FUNCTIONS": { + "TITLE": "함수", + "DESCRIPTION": "Zitadel 내에서 호출할 수 있는 함수입니다. 이는 이메일 전송부터 사용자 생성까지 모든 것이 될 수 있습니다." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "이 작업이 모든 요청, 특정 서비스(예: 사용자 관리) 또는 단일 요청(예: 사용자 생성)에 적용되는지 선택하십시오.", + "ALL": { + "TITLE": "모두", + "DESCRIPTION": "모든 요청에서 작업을 실행하려면 이것을 선택하십시오." + }, + "ALL_EVENTS": "모든 이벤트에서 작업을 실행하려면 이 항목을 선택하세요", + "SELECT_SERVICE": { + "TITLE": "서비스 선택", + "DESCRIPTION": "작업에 대한 Zitadel 서비스를 선택하십시오." + }, + "SELECT_METHOD": { + "TITLE": "메서드 선택", + "DESCRIPTION": "특정 요청에서만 실행하려면 여기에서 선택하십시오.", + "NOTE": "메서드를 선택하지 않으면 선택한 서비스의 모든 요청에서 작업이 실행됩니다." + }, + "FUNCTIONNAME": { + "TITLE": "함수 이름", + "DESCRIPTION": "실행할 함수를 선택하십시오." + }, + "SELECT_GROUP": { + "TITLE": "그룹 설정", + "DESCRIPTION": "이벤트 그룹에서만 실행하려면 여기에서 그룹을 설정하십시오." + }, + "SELECT_EVENT": { + "TITLE": "이벤트 선택", + "DESCRIPTION": "특정 이벤트에서만 실행하려면 여기에서 지정하십시오." + } + }, + "TARGET": { + "DESCRIPTION": "타겟을 실행하거나 다른 타겟과 동일한 조건으로 실행하도록 선택할 수 있습니다.", + "TARGET": { + "DESCRIPTION": "이 작업에 대해 실행할 타겟" + }, + "CONDITIONS": { + "DESCRIPTION": "실행 조건" + } + } + }, + "TABLE": { + "CONDITION": "조건", + "TYPE": "유형", + "TARGET": "타겟", + "CREATIONDATE": "생성 날짜" + } + }, + "TARGET": { + "TITLE": "타겟", + "DESCRIPTION": "타겟은 작업에서 실행하려는 코드의 대상입니다. 여기에서 타겟을 생성하고 작업에 추가하십시오.", + "CREATE": { + "TITLE": "타겟 생성", + "DESCRIPTION": "Zitadel 외부에서 자체 타겟을 생성하십시오.", + "NAME": "이름", + "NAME_DESCRIPTION": "나중에 쉽게 식별할 수 있도록 타겟에 명확하고 설명적인 이름을 지정하십시오.", + "TYPE": "유형", + "TYPES": { + "restWebhook": "REST 웹훅", + "restCall": "REST 호출", + "restAsync": "REST 비동기" + }, + "TYPES_DESCRIPTION": "Webhook, 호출은 상태 코드를 처리하지만 응답은 중요하지 않습니다\nCall, 호출은 상태 코드와 응답을 처리합니다\nAsync, 호출은 상태 코드나 응답을 처리하지 않지만 다른 대상과 병렬로 호출할 수 있습니다", + "ENDPOINT": "엔드포인트", + "ENDPOINT_DESCRIPTION": "코드가 호스팅되는 엔드포인트를 입력하십시오. 우리에게 액세스할 수 있는지 확인하십시오!", + "TIMEOUT": "시간 초과", + "TIMEOUT_DESCRIPTION": "타겟이 응답해야 하는 최대 시간을 설정하십시오. 시간이 더 오래 걸리면 요청을 중지합니다.", + "INTERRUPT_ON_ERROR": "오류 시 중단", + "INTERRUPT_ON_ERROR_DESCRIPTION": "타겟이 오류를 반환하면 모든 실행을 중지하십시오.", + "INTERRUPT_ON_ERROR_WARNING": "주의: “오류 시 중단” 기능은 실패 시 작업을 중단하며, 잠금 위험이 있습니다. 로그인/생성 차단을 방지하려면 비활성화된 상태로 테스트하세요.", + "AWAIT_RESPONSE": "응답 대기", + "AWAIT_RESPONSE_DESCRIPTION": "다른 작업을 수행하기 전에 응답을 기다립니다. 단일 작업에 여러 타겟을 사용하려는 경우 유용합니다." + }, + "TABLE": { + "NAME": "이름", + "ENDPOINT": "엔드포인트", + "CREATIONDATE": "생성 날짜", + "REORDER": "재정렬" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "인스턴스와 모든 조직에 대한 제어 권한이 있습니다", "IAM_OWNER_VIEWER": "인스턴스와 모든 조직을 검토할 수 있는 권한이 있습니다", @@ -790,7 +929,10 @@ "PHONESECTION": "전화번호", "PASSWORDSECTION": "초기 비밀번호", "ADDRESSANDPHONESECTION": "전화번호", - "INITMAILDESCRIPTION": "두 옵션이 모두 선택된 경우 초기화 이메일이 전송되지 않습니다. 하나의 옵션만 선택된 경우 데이터 제공/확인을 위한 이메일이 전송됩니다." + "INITMAILDESCRIPTION": "두 옵션이 모두 선택된 경우 초기화 이메일이 전송되지 않습니다. 하나의 옵션만 선택된 경우 데이터 제공/확인을 위한 이메일이 전송됩니다.", + "SETUPAUTHENTICATIONLATER": "이 사용자의 인증을 나중에 설정하세요.", + "INVITATION": "인증 설정 및 이메일 확인을 위한 초대 이메일을 보내세요.", + "INITIALPASSWORD": "사용자에 대한 초기 비밀번호를 설정하세요." }, "CODEDIALOG": { "TITLE": "전화번호 확인", @@ -1354,6 +1496,7 @@ "BRANDING": "브랜딩", "PRIVACYPOLICY": "외부 링크", "OIDC": "OIDC 토큰 수명 및 만료", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "시크릿 생성기", "SECURITY": "보안 설정", "EVENTS": "이벤트", @@ -1369,7 +1512,8 @@ "APPEARANCE": "외형", "OTHER": "기타", "STORAGE": "저장소" - } + }, + "BETA": "베타" }, "SETTING": { "LANGUAGES": { @@ -1399,7 +1543,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1490,6 +1635,16 @@ "ACTIONS_DESCRIPTION": "액션 v2는 데이터 실행 및 대상을 관리할 수 있습니다. 플래그가 활성화되면 새 API 및 기능을 사용할 수 있습니다.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC 단일 V1 세션 종료", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "플래그가 활성화되면, `sid` 클레임이 있는 id_token을 사용하여 end_session 엔드포인트에서 로그인 UI의 단일 세션을 종료할 수 있습니다. 현재 동일한 사용자 에이전트(브라우저)에서 모든 세션이 로그인 UI에서 종료됩니다. Session API를 통해 관리된 세션은 이미 단일 세션 종료를 허용합니다.", + "DEBUGOIDCPARENTERROR": "디버그 OIDC 부모 오류", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "플래그가 활성화되면 OIDC 부모 오류가 콘솔에 기록됩니다.", + "DISABLEUSERTOKENEVENT": "사용자 토큰 이벤트 비활성화", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "백채널 로그아웃 활성화", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "백채널 로그아웃은 OpenID Connect 백채널 로그아웃 1.0을 구현하며, OpenID 제공자에서 세션 종료에 대해 클라이언트에게 알리는 데 사용할 수 있습니다.", + "PERMISSIONCHECKV2": "권한 확인 V2", + "PERMISSIONCHECKV2_DESCRIPTION": "플래그가 활성화되면 새로운 API와 그 기능을 사용할 수 있습니다.", + "WEBKEY": "웹 키", + "WEBKEY_DESCRIPTION": "플래그가 활성화되면 새로운 API와 그 기능을 사용할 수 있습니다.", "STATES": { "INHERITED": "상속", "ENABLED": "활성화됨", @@ -1502,7 +1657,10 @@ }, "RESET": "모두 상속으로 설정", "CONSOLEUSEV2USERAPI": "콘솔에서 사용자 생성을 위해 V2 API를 사용하세요", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "이 플래그가 활성화되면 콘솔은 V2 사용자 API를 사용하여 새 사용자를 생성합니다. V2 API를 사용하면 새로 생성된 사용자는 초기 상태 없이 시작합니다." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "이 플래그가 활성화되면 콘솔은 V2 사용자 API를 사용하여 새 사용자를 생성합니다. V2 API를 사용하면 새로 생성된 사용자는 초기 상태 없이 시작합니다.", + "LOGINV2": "로그인 V2", + "LOGINV2_DESCRIPTION": "이 옵션을 활성화하면 보안, 성능 및 사용자 정의 기능이 향상된 새로운 TypeScript 기반 로그인 UI가 활성화됩니다.", + "LOGINV2_BASEURI": "기본 URI" }, "DIALOG": { "RESET": { @@ -1639,7 +1797,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "이메일 인증 완료", @@ -2609,7 +2769,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "매니저 추가", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 6f7b30b1c6..2e62723939 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Текови", "DESCRIPTION": "Изберете тек на автентификација и активирајте ја вашата акција на специфичен настан во тој тек." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, нова и подобрена верзија на Actions, сега е достапна. Сегашната верзија сè уште е достапна, но идниот развој ќе биде насочен кон новата верзија, која на крајот ќе ја замени сегашната." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "Животниот век на неактивниот токен за освежување е максималното време кое токен за освежување може да не се користи." } }, + "WEB_KEYS": { + "DESCRIPTION": "Управувајте со вашите OIDC веб-клучеви за безбедно потпишување и валидација на токени за вашата ZITADEL инстанца.", + "TABLE": { + "TITLE": "Активни и Идни Веб-Клучеви", + "DESCRIPTION": "Вашите активни и претстојни веб-клучеви. Активирањето на нов клуч ќе го деактивира тековниот.", + "NOTE": "Забелешка: JWKs OIDC крајната точка враќа одговор што може да се кешира (стандардно 5 минути). Избегнувајте активирање на клучот пребрзо, бидејќи можеби сè уште не е достапен во кешот и кај клиентите.", + "ACTIVATE": "Активирај го следниот веб-клуч", + "ACTIVE": "Моментално активен", + "NEXT": "Следен во редот", + "FUTURE": "Иднина", + "WARNING": "Веб-клучот е помалку од 5 минути стар" + }, + "CREATE": { + "TITLE": "Креирај нов веб-клуч", + "DESCRIPTION": "Креирањето нов веб-клуч го додава на вашата листа. ZITADEL стандардно користи RSA2048 клучеви со SHA256 алгоритам за хаширање.", + "KEY_TYPE": "Тип на клуч", + "BITS": "Битови", + "HASHER": "Алгоритам за хаширање", + "CURVE": "Крива" + }, + "PREVIOUS_TABLE": { + "TITLE": "Претходни веб-клучеви", + "DESCRIPTION": "Ова се вашите претходни веб-клучеви кои повеќе не се активни.", + "DEACTIVATED_ON": "Деактивиран на" + } + }, "MESSAGE_TEXTS": { "TITLE": "Текстови на пораки", "DESCRIPTION": "Прилагодете ги текстовите на вашите е-маил или SMS пораки за известување. Ако сакате да оневозможите некои јазици, ограничете ги во поставките за јазик на вашите инстанци.", @@ -502,6 +529,118 @@ "DOWNLOAD": "Преземи", "APPLY": "Пријавете се" }, + "ACTIONSTWO": { + "BETA_NOTE": "", + "EXECUTION": { + "TITLE": "Акции", + "DESCRIPTION": "Акциите ви овозможуваат да извршувате прилагоден код како одговор на API барања, настани или специфични функции. Користете ги за да го проширите Zitadel, да ги автоматизирате работните процеси и да се интегрирате со други системи.", + "TYPES": { + "request": "Барање", + "response": "Одговор", + "event": "Настани", + "function": "Функција" + }, + "DIALOG": { + "CREATE_TITLE": "Креирај акција", + "UPDATE_TITLE": "Ажурирај акција", + "TYPE": { + "DESCRIPTION": "Изберете кога сакате да се изврши оваа акција", + "REQUEST": { + "TITLE": "Барање", + "DESCRIPTION": "Барања што се случуваат во Zitadel. Ова може да биде нешто како повик за барање за најава." + }, + "RESPONSE": { + "TITLE": "Одговор", + "DESCRIPTION": "Одговор од барање во Zitadel. Размислете за одговорот што го добивате при преземање на корисник." + }, + "EVENTS": { + "TITLE": "Настани", + "DESCRIPTION": "Настани што се случуваат во Zitadel. Ова може да биде нешто како корисник што креира сметка, успешна најава итн." + }, + "FUNCTIONS": { + "TITLE": "Функции", + "DESCRIPTION": "Функции што можете да ги повикате во Zitadel. Ова може да биде сè, од испраќање е-пошта до креирање корисник." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Изберете дали оваа акција се однесува на сите барања, одредена услуга (на пр. управување со корисници) или едно барање (на пр. креирај корисник).", + "ALL": { + "TITLE": "Сите", + "DESCRIPTION": "Изберете го ова ако сакате да ја извршите вашата акција на секое барање" + }, + "ALL_EVENTS": "Изберете го ова ако сакате вашата акција да се извршува на секој настан", + "SELECT_SERVICE": { + "TITLE": "Изберете услуга", + "DESCRIPTION": "Изберете Zitadel услуга за вашата акција." + }, + "SELECT_METHOD": { + "TITLE": "Изберете метод", + "DESCRIPTION": "Ако сакате да извршите само на одредено барање, изберете го тука", + "NOTE": "Ако не изберете метод, вашата акција ќе се изврши на секое барање во вашата избрана услуга." + }, + "FUNCTIONNAME": { + "TITLE": "Име на функција", + "DESCRIPTION": "Изберете ја функцијата што сакате да ја извршите" + }, + "SELECT_GROUP": { + "TITLE": "Постави група", + "DESCRIPTION": "Ако сакате да извршите само на група настани, поставете ја групата тука" + }, + "SELECT_EVENT": { + "TITLE": "Изберете настан", + "DESCRIPTION": "Ако сакате да извршите само на одреден настан, наведете го тука" + } + }, + "TARGET": { + "DESCRIPTION": "Можете да изберете да извршите цел или да ја извршите под истите услови како и другите цели.", + "TARGET": { + "DESCRIPTION": "Целта што сакате да ја извршите за оваа акција" + }, + "CONDITIONS": { + "DESCRIPTION": "Услови за извршување" + } + } + }, + "TABLE": { + "CONDITION": "Услов", + "TYPE": "Тип", + "TARGET": "Цел", + "CREATIONDATE": "Датум на создавање" + } + }, + "TARGET": { + "TITLE": "Цели", + "DESCRIPTION": "Целта е дестинација на кодот што сакате да го извршите од акција. Креирајте цел овде и додајте ја на вашите акции.", + "CREATE": { + "TITLE": "Креирајте ја вашата цел", + "DESCRIPTION": "Креирајте ја вашата сопствена цел надвор од Zitadel", + "NAME": "Име", + "NAME_DESCRIPTION": "Дајте ѝ на вашата цел јасно, опис на име за да биде лесно да се идентификува подоцна", + "TYPE": "Тип", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Повик", + "restAsync": "REST Асинхроно" + }, + "TYPES_DESCRIPTION": "Webhook, повикот го обработува статусниот код но одговорот е ирелевантен\nCall, повикот го обработува статусниот код и одговорот\nAsync, повикот не го обработува ниту статусниот код ниту одговорот, но може да се повика паралелно со други цели", + "ENDPOINT": "Крајна точка", + "ENDPOINT_DESCRIPTION": "Внесете ја крајната точка каде што е хостиран вашиот код. Осигурете се дека е достапна за нас!", + "TIMEOUT": "Време на истекување", + "TIMEOUT_DESCRIPTION": "Поставете го максималното време што вашата цел треба да одговори. Ако трае подолго, ќе го запреме барањето.", + "INTERRUPT_ON_ERROR": "Прекини при грешка", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Запрете ги сите извршувања кога целите ќе вратат грешка", + "INTERRUPT_ON_ERROR_WARNING": "Внимание: „Прекини при грешка“ ги запира операциите при неуспех, со ризик од блокирање. Тестирајте со исклучена опција за да избегнете блокирање на најавата/креирањето.", + "AWAIT_RESPONSE": "Почекај одговор", + "AWAIT_RESPONSE_DESCRIPTION": "Ќе почекаме одговор пред да направиме нешто друго. Корисно ако планирате да користите повеќе цели за една акција" + }, + "TABLE": { + "NAME": "Име", + "ENDPOINT": "Крајна точка", + "CREATIONDATE": "Датум на создавање", + "REORDER": "Повторно нарачајте" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Има контрола врз целата инстанца, вклучувајќи ги сите организации", "IAM_OWNER_VIEWER": "Има дозвола за преглед на целата инстанца, вклучувајќи ги сите организации", @@ -790,7 +929,10 @@ "PHONESECTION": "Телефонски броеви", "PASSWORDSECTION": "Почетна лозинка", "ADDRESSANDPHONESECTION": "Телефонски број", - "INITMAILDESCRIPTION": "Ако се изберат двете опции, нема да се испрати е-пошта за иницијализација. Ако се избере само една од опциите, ќе биде испратена е-пошта за обезбедување / верификација на податоците." + "INITMAILDESCRIPTION": "Ако се изберат двете опции, нема да се испрати е-пошта за иницијализација. Ако се избере само една од опциите, ќе биде испратена е-пошта за обезбедување / верификација на податоците.", + "SETUPAUTHENTICATIONLATER": "Подесете автентикација подоцна за овој корисник.", + "INVITATION": "Испратете покана по е-пошта за поставување на автентикација и потврда на е-поштата.", + "INITIALPASSWORD": "Поставете почетна лозинка за корисникот." }, "CODEDIALOG": { "TITLE": "Верификација на телефонски број", @@ -1355,6 +1497,7 @@ "BRANDING": "Брендирање", "PRIVACYPOLICY": "Политика за приватност", "OIDC": "OIDC времетраење и истекување на токени", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Изглед на тајни", "SECURITY": "Подесувања за безбедност", "EVENTS": "Настани", @@ -1370,7 +1513,8 @@ "APPEARANCE": "Изглед", "OTHER": "Друго", "STORAGE": "складирање" - } + }, + "BETA": "БЕТА" }, "SETTING": { "LANGUAGES": { @@ -1400,7 +1544,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1491,6 +1636,16 @@ "ACTIONS_DESCRIPTION": "Акциите v2 овозможуваат управување со извршување на податоци и цели. Ако знамето е овозможено, ќе можете да го користите новиот API и неговите функции.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Завршување на сесија", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Ако ознаката е активирана, ќе можете да ја завршите единечна сесија од корисничкиот интерфејс за најава, со обезбедување id_token со `sid` побарување како id_token_hint на крајната точка на end_session. Имајте предвид дека во моментов сите сесии од истиот кориснички агент (прелистувач) се завршуваат во корисничкиот интерфејс за најава. Сесиите управувани преку API на сесија веќе дозволуваат завршување на единечни сесии.", + "DEBUGOIDCPARENTERROR": "Дебагирање на OIDC родителска грешка", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Ако знамето е овозможено, грешката на OIDC родителот ќе биде регистрирана во конзолата.", + "DISABLEUSERTOKENEVENT": "Оневозможи настан за кориснички токен", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Овозможи Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout имплементира OpenID Connect Back-Channel Logout 1.0 и може да се користи за известување на клиентите за завршување на сесијата кај OpenID провајдерот.", + "PERMISSIONCHECKV2": "Проверка на дозволи V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Ако знамето е овозможено, ќе можете да ја користите новата API и нејзините функции.", + "WEBKEY": "Веб клуч", + "WEBKEY_DESCRIPTION": "Ако знамето е овозможено, ќе можете да ја користите новата API и нејзините функции.", "STATES": { "INHERITED": "Наследи", "ENABLED": "Овозможено", @@ -1503,7 +1658,10 @@ }, "RESET": "Поставете ги сите да наследат", "CONSOLEUSEV2USERAPI": "Користете V2 API во конзолата за креирање на корисници", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Кога ова знаме е овозможено, конзолата го користи V2 User API за креирање на нови корисници. Со V2 API, новосоздадените корисници започнуваат без почетна состојба." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Кога ова знаме е овозможено, конзолата го користи V2 User API за креирање на нови корисници. Со V2 API, новосоздадените корисници започнуваат без почетна состојба.", + "LOGINV2": "Најава V2", + "LOGINV2_DESCRIPTION": "Овозможувањето на ова ја активира новата TypeScript-базирана најава со подобрена безбедност, перформанси и прилагодливост.", + "LOGINV2_BASEURI": "Основен URI" }, "DIALOG": { "RESET": { @@ -1640,7 +1798,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Е-поштата е верифицирана", @@ -2585,7 +2745,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Додај Менаџер", diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index 2a50ddc1ac..7e549f64ba 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Stromen", "DESCRIPTION": "Kies een authenticatiestroom en activeer je actie bij een specifieke gebeurtenis binnen deze stroom." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, een nieuwe en verbeterde versie van Actions, is nu beschikbaar. De huidige versie blijft toegankelijk, maar onze toekomstige ontwikkeling zal zich richten op de nieuwe versie, die uiteindelijk de huidige zal vervangen." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "De levensduur van het inactieve vernieuwingstoken is de maximale tijd dat een vernieuwingstoken ongebruikt kan zijn." } }, + "WEB_KEYS": { + "DESCRIPTION": "Beheer je OIDC Web Keys om tokens veilig te ondertekenen en te valideren voor je ZITADEL-instantie.", + "TABLE": { + "TITLE": "Actieve en Toekomstige Websleutels", + "DESCRIPTION": "Je actieve en aankomende websleutels. Het activeren van een nieuwe sleutel deactiveert de huidige.", + "NOTE": "Opmerking: Het JWKs OIDC-eindpunt geeft een cachebare respons terug (standaard 5 minuten). Vermijd het te vroeg activeren van een sleutel, omdat deze mogelijk nog niet beschikbaar is in caches en bij clients.", + "ACTIVATE": "Volgende Websleutel activeren", + "ACTIVE": "Momenteel actief", + "NEXT": "Volgende in de wachtrij", + "FUTURE": "Toekomstig", + "WARNING": "De websleutel is minder dan 5 minuten oud" + }, + "CREATE": { + "TITLE": "Nieuwe Websleutel aanmaken", + "DESCRIPTION": "Het aanmaken van een nieuwe websleutel voegt deze toe aan je lijst. ZITADEL gebruikt standaard RSA2048-sleutels met een SHA256-hasher.", + "KEY_TYPE": "Sleuteltype", + "BITS": "Bits", + "HASHER": "Hasher", + "CURVE": "Curve" + }, + "PREVIOUS_TABLE": { + "TITLE": "Vorige Websleutels", + "DESCRIPTION": "Dit zijn je vorige websleutels die niet langer actief zijn.", + "DEACTIVATED_ON": "Gedeactiveerd op" + } + }, "MESSAGE_TEXTS": { "TITLE": "Berichtteksten", "DESCRIPTION": "Pas de teksten van je notificatie-e-mail of SMS-berichten aan. Als je sommige talen wilt uitschakelen, beperk ze dan in de taalinstellingen van je instanties.", @@ -502,6 +529,118 @@ "DOWNLOAD": "Download", "APPLY": "Toepassen" }, + "ACTIONSTWO": { + "BETA_NOTE": "U gebruikt momenteel de nieuwe Actions V2, die zich in de bètaversie bevindt. De vorige versie 1 is nog beschikbaar maar zal in de toekomst worden stopgezet. Meld alstublieft eventuele problemen of feedback.", + "EXECUTION": { + "TITLE": "Acties", + "DESCRIPTION": "Met acties kunt u aangepaste code uitvoeren als reactie op API-verzoeken, gebeurtenissen of specifieke functies. Gebruik ze om Zitadel uit te breiden, workflows te automatiseren en te integreren met andere systemen.", + "TYPES": { + "request": "Verzoek", + "response": "Reactie", + "event": "Gebeurtenissen", + "function": "Functie" + }, + "DIALOG": { + "CREATE_TITLE": "Een actie maken", + "UPDATE_TITLE": "Een actie bijwerken", + "TYPE": { + "DESCRIPTION": "Selecteer wanneer u deze actie wilt uitvoeren", + "REQUEST": { + "TITLE": "Verzoek", + "DESCRIPTION": "Verzoeken die binnen Zitadel plaatsvinden. Dit kan zoiets zijn als een inlogverzoek-oproep." + }, + "RESPONSE": { + "TITLE": "Reactie", + "DESCRIPTION": "Een reactie op een verzoek binnen Zitadel. Denk aan de reactie die u terugkrijgt van het ophalen van een gebruiker." + }, + "EVENTS": { + "TITLE": "Gebeurtenissen", + "DESCRIPTION": "Gebeurtenissen die binnen Zitadel plaatsvinden. Dit kan van alles zijn, zoals een gebruiker die een account aanmaakt, een succesvolle login enz." + }, + "FUNCTIONS": { + "TITLE": "Functies", + "DESCRIPTION": "Functies die u binnen Zitadel kunt aanroepen. Dit kan van alles zijn, van het verzenden van een e-mail tot het maken van een gebruiker." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Kies of deze actie van toepassing is op alle verzoeken, een specifieke service (bijv. gebruikersbeheer) of een enkel verzoek (bijv. gebruiker maken).", + "ALL": { + "TITLE": "Alle", + "DESCRIPTION": "Selecteer dit als u uw actie bij elk verzoek wilt uitvoeren" + }, + "ALL_EVENTS": "Selecteer dit als je je actie bij elk evenement wilt uitvoeren", + "SELECT_SERVICE": { + "TITLE": "Service selecteren", + "DESCRIPTION": "Kies een Zitadel-service voor uw actie." + }, + "SELECT_METHOD": { + "TITLE": "Methode selecteren", + "DESCRIPTION": "Als u alleen bij een specifiek verzoek wilt uitvoeren, selecteert u dit hier", + "NOTE": "Als u geen methode selecteert, wordt uw actie bij elk verzoek in uw geselecteerde service uitgevoerd." + }, + "FUNCTIONNAME": { + "TITLE": "Functienaam", + "DESCRIPTION": "Kies de functie die u wilt uitvoeren" + }, + "SELECT_GROUP": { + "TITLE": "Groep instellen", + "DESCRIPTION": "Als u alleen bij een groep gebeurtenissen wilt uitvoeren, stelt u de groep hier in" + }, + "SELECT_EVENT": { + "TITLE": "Gebeurtenis selecteren", + "DESCRIPTION": "Als u alleen bij een specifieke gebeurtenis wilt uitvoeren, specificeert u deze hier" + } + }, + "TARGET": { + "DESCRIPTION": "U kunt ervoor kiezen om een doel uit te voeren of om het onder dezelfde voorwaarden als andere doelen uit te voeren.", + "TARGET": { + "DESCRIPTION": "Het doel dat u voor deze actie wilt uitvoeren" + }, + "CONDITIONS": { + "DESCRIPTION": "Uitvoeringsvoorwaarden" + } + } + }, + "TABLE": { + "CONDITION": "Voorwaarde", + "TYPE": "Type", + "TARGET": "Doel", + "CREATIONDATE": "Aanmaakdatum" + } + }, + "TARGET": { + "TITLE": "Doelen", + "DESCRIPTION": "Een doel is de bestemming van de code die u vanuit een actie wilt uitvoeren. Maak hier een doel en voeg het toe aan uw acties.", + "CREATE": { + "TITLE": "Uw doel maken", + "DESCRIPTION": "Maak uw eigen doel buiten Zitadel", + "NAME": "Naam", + "NAME_DESCRIPTION": "Geef uw doel een duidelijke, beschrijvende naam om het later gemakkelijk te kunnen identificeren", + "TYPE": "Type", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Aanroep", + "restAsync": "REST Asynchroon" + }, + "TYPES_DESCRIPTION": "Webhook, de oproep verwerkt de statuscode maar de reactie is irrelevant\nCall, de oproep verwerkt de statuscode en de reactie\nAsync, de oproep verwerkt noch de statuscode noch de reactie, maar kan parallel aan andere doelen worden aangeroepen", + "ENDPOINT": "Eindpunt", + "ENDPOINT_DESCRIPTION": "Voer het eindpunt in waar uw code wordt gehost. Zorg ervoor dat het voor ons toegankelijk is!", + "TIMEOUT": "Time-out", + "TIMEOUT_DESCRIPTION": "Stel de maximale tijd in die uw doel heeft om te reageren. Als het langer duurt, stoppen we het verzoek.", + "INTERRUPT_ON_ERROR": "Onderbreken bij fout", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Stop alle uitvoeringen als de doelen een fout retourneren", + "INTERRUPT_ON_ERROR_WARNING": "Let op: “Onderbreken bij fout” stopt operaties bij een mislukking, met kans op blokkering. Test met deze optie uitgeschakeld om inloggen/aanmaken niet te blokkeren.", + "AWAIT_RESPONSE": "Wachten op reactie", + "AWAIT_RESPONSE_DESCRIPTION": "We wachten op een reactie voordat we iets anders doen. Handig als u van plan bent om meerdere doelen voor één actie te gebruiken" + }, + "TABLE": { + "NAME": "Naam", + "ENDPOINT": "Eindpunt", + "CREATIONDATE": "Aanmaakdatum", + "REORDER": "Opnieuw ordenen" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Heeft controle over de hele instantie, inclusief alle organisaties", "IAM_OWNER_VIEWER": "Heeft toestemming om de hele instantie te bekijken, inclusief alle organisaties", @@ -790,7 +929,10 @@ "PHONESECTION": "Telefoonnummers", "PASSWORDSECTION": "Initieel wachtwoord", "ADDRESSANDPHONESECTION": "Telefoonnummer", - "INITMAILDESCRIPTION": "Als beide opties geselecteerd zijn, wordt er geen e-mail voor initialisatie verzonden. Als slechts een van de opties is geselecteerd, wordt een e-mail gestuurd om de gegevens te verstrekken / te verifiëren." + "INITMAILDESCRIPTION": "Als beide opties geselecteerd zijn, wordt er geen e-mail voor initialisatie verzonden. Als slechts een van de opties is geselecteerd, wordt een e-mail gestuurd om de gegevens te verstrekken / te verifiëren.", + "SETUPAUTHENTICATIONLATER": "Authenticatie later instellen voor deze gebruiker.", + "INVITATION": "Stuur een uitnodigingsmail voor het instellen van authenticatie en e-mailverificatie.", + "INITIALPASSWORD": "Stel een initieel wachtwoord in voor de gebruiker." }, "CODEDIALOG": { "TITLE": "Verifieer telefoonnummer", @@ -1354,6 +1496,7 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Privacybeleid", "OIDC": "OIDC Token levensduur en vervaldatum", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Secret Generator", "SECURITY": "Beveiligingsinstellingen", "EVENTS": "Evenementen", @@ -1369,7 +1512,8 @@ "APPEARANCE": "Verschijning", "OTHER": "Andere", "STORAGE": "opslag" - } + }, + "BETA": "BÈTA" }, "SETTING": { "LANGUAGES": { @@ -1397,7 +1541,10 @@ "ru": "Русский", "nl": "Nederlands", "sv": "Svenska", - "hu": "Magyar" + "id": "Bahasa Indonesia", + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1488,6 +1635,16 @@ "ACTIONS_DESCRIPTION": "Actions v2 maken het mogelijk om data-uitvoeringen en doelen te beheren. Als de vlag is ingeschakeld, kunt u de nieuwe API en zijn functies gebruiken.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Sessiebeëindiging", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Als het vlagje is ingeschakeld, kunt u een enkele sessie beëindigen via de login-gebruikersinterface door een id_token met een `sid`-claim als id_token_hint op het eindpunt end_session te verstrekken. Houd er rekening mee dat momenteel alle sessies van dezelfde gebruikersagent (browser) worden beëindigd in de login-gebruikersinterface. Sessies die worden beheerd via de Session API staan al toe om individuele sessies te beëindigen.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Ouderfout", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Als de vlag is ingeschakeld, wordt de OIDC-ouderfout in de console geregistreerd.", + "DISABLEUSERTOKENEVENT": "Gebruikerstokengebeurtenis uitschakelen", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Backchannel Logout inschakelen", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "De Back-Channel Logout implementeert OpenID Connect Back-Channel Logout 1.0 en kan worden gebruikt om clients te informeren over het beëindigen van de sessie bij de OpenID-provider.", + "PERMISSIONCHECKV2": "Permissiecontrole V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Als de vlag is ingeschakeld, kunt u de nieuwe API en de bijbehorende functies gebruiken.", + "WEBKEY": "Websleutel", + "WEBKEY_DESCRIPTION": "Als de vlag is ingeschakeld, kunt u de nieuwe API en de bijbehorende functies gebruiken.", "STATES": { "INHERITED": "Overgenomen", "ENABLED": "Ingeschakeld", @@ -1500,7 +1657,10 @@ }, "RESET": "Alles instellen op overgenomen", "CONSOLEUSEV2USERAPI": "Gebruik de V2 API in de console voor het aanmaken van gebruikers", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Wanneer deze vlag is ingeschakeld, gebruikt de console de V2 User API om nieuwe gebruikers aan te maken. Met de V2 API beginnen nieuw aangemaakte gebruikers zonder een initiële status." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Wanneer deze vlag is ingeschakeld, gebruikt de console de V2 User API om nieuwe gebruikers aan te maken. Met de V2 API beginnen nieuw aangemaakte gebruikers zonder een initiële status.", + "LOGINV2": "Inloggen V2", + "LOGINV2_DESCRIPTION": "Door dit in te schakelen wordt de nieuwe TypeScript-gebaseerde login-UI geactiveerd met verbeterde beveiliging, prestaties en aanpasbaarheid.", + "LOGINV2_BASEURI": "Basis-URI" }, "DIALOG": { "RESET": { @@ -1637,7 +1797,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "E-mail verificatie voltooid", @@ -2604,7 +2766,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Voeg een Manager toe", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 30575f67b0..2f18c343f7 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Przepływy", "DESCRIPTION": "Wybierz przepływ uwierzytelniania i wywołaj swoją akcję przy określonym zdarzeniu w tym przepływie." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, nowa, ulepszona wersja Actions, jest już dostępna. Obecna wersja jest nadal dostępna, ale przyszły rozwój będzie skoncentrowany na nowej wersji, która ostatecznie zastąpi obecną." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "Czas życia bezczynnego tokena odświeżania to maksymalny czas, przez który token odświeżania może pozostać nieużywany." } }, + "WEB_KEYS": { + "DESCRIPTION": "Zarządzaj swoimi kluczami internetowymi OIDC, aby bezpiecznie podpisywać i weryfikować tokeny w swojej instancji ZITADEL.", + "TABLE": { + "TITLE": "Aktywne i Przyszłe Klucze Internetowe", + "DESCRIPTION": "Twoje aktywne i nadchodzące klucze internetowe. Aktywacja nowego klucza spowoduje dezaktywację obecnego.", + "NOTE": "Uwaga: Punkt końcowy JWKs OIDC zwraca odpowiedź możliwą do buforowania (domyślnie 5 minut). Unikaj zbyt wczesnej aktywacji klucza, ponieważ może on nie być jeszcze dostępny w pamięci podręcznej i dla klientów.", + "ACTIVATE": "Aktywuj następny klucz internetowy", + "ACTIVE": "Obecnie aktywny", + "NEXT": "Następny w kolejce", + "FUTURE": "Przyszłe", + "WARNING": "Klucz sieciowy ma mniej niż 5 minut" + }, + "CREATE": { + "TITLE": "Utwórz nowy klucz internetowy", + "DESCRIPTION": "Utworzenie nowego klucza internetowego doda go do Twojej listy. ZITADEL domyślnie używa kluczy RSA2048 z haszowaniem SHA256.", + "KEY_TYPE": "Typ klucza", + "BITS": "Bity", + "HASHER": "Haszowanie", + "CURVE": "Krzywa" + }, + "PREVIOUS_TABLE": { + "TITLE": "Poprzednie Klucze Internetowe", + "DESCRIPTION": "To są Twoje poprzednie klucze internetowe, które nie są już aktywne.", + "DEACTIVATED_ON": "Dezaktywowany dnia" + } + }, "MESSAGE_TEXTS": { "TITLE": "Teksty wiadomości", "DESCRIPTION": "Dostosuj teksty swoich e-maili lub wiadomości SMS z powiadomieniami. Jeśli chcesz wyłączyć niektóre języki, ogranicz je w ustawieniach językowych swoich instancji.", @@ -501,6 +528,118 @@ "DOWNLOAD": "Pobierz", "APPLY": "Stosować" }, + "ACTIONSTWO": { + "BETA_NOTE": "Obecnie korzystasz z nowej wersji Actions V2, która jest w fazie beta. Poprzednia wersja 1 jest nadal dostępna, ale w przyszłości zostanie wycofana. Prosimy o zgłaszanie wszelkich problemów lub opinii.", + "EXECUTION": { + "TITLE": "Akcje", + "DESCRIPTION": "Akcje umożliwiają uruchamianie niestandardowego kodu w odpowiedzi na żądania API, zdarzenia lub określone funkcje. Użyj ich, aby rozszerzyć Zitadel, zautomatyzować przepływy pracy i zintegrować się z innymi systemami.", + "TYPES": { + "request": "Żądanie", + "response": "Odpowiedź", + "event": "Zdarzenia", + "function": "Funkcja" + }, + "DIALOG": { + "CREATE_TITLE": "Utwórz akcję", + "UPDATE_TITLE": "Aktualizuj akcję", + "TYPE": { + "DESCRIPTION": "Wybierz, kiedy chcesz uruchomić tę akcję", + "REQUEST": { + "TITLE": "Żądanie", + "DESCRIPTION": "Żądania występujące w Zitadel. Może to być coś takiego jak wywołanie żądania logowania." + }, + "RESPONSE": { + "TITLE": "Odpowiedź", + "DESCRIPTION": "Odpowiedź na żądanie w Zitadel. Pomyśl o odpowiedzi, którą otrzymujesz po pobraniu użytkownika." + }, + "EVENTS": { + "TITLE": "Zdarzenia", + "DESCRIPTION": "Zdarzenia, które mają miejsce w Zitadel. Mogą to być dowolne zdarzenia, takie jak utworzenie konta użytkownika, udane logowanie itp." + }, + "FUNCTIONS": { + "TITLE": "Funkcje", + "DESCRIPTION": "Funkcje, które można wywołać w Zitadel. Mogą to być dowolne funkcje, od wysłania wiadomości e-mail po utworzenie użytkownika." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Wybierz, czy ta akcja ma zastosowanie do wszystkich żądań, określonej usługi (np. zarządzanie użytkownikami) czy pojedynczego żądania (np. utwórz użytkownika).", + "ALL": { + "TITLE": "Wszystkie", + "DESCRIPTION": "Wybierz tę opcję, jeśli chcesz uruchomić akcję dla każdego żądania" + }, + "ALL_EVENTS": "Wybierz to, jeśli chcesz uruchamiać swoją akcję przy każdym zdarzeniu", + "SELECT_SERVICE": { + "TITLE": "Wybierz usługę", + "DESCRIPTION": "Wybierz usługę Zitadel dla swojej akcji." + }, + "SELECT_METHOD": { + "TITLE": "Wybierz metodę", + "DESCRIPTION": "Jeśli chcesz uruchomić tylko dla określonego żądania, wybierz je tutaj", + "NOTE": "Jeśli nie wybierzesz metody, akcja zostanie uruchomiona dla każdego żądania w wybranej usłudze." + }, + "FUNCTIONNAME": { + "TITLE": "Nazwa funkcji", + "DESCRIPTION": "Wybierz funkcję, którą chcesz uruchomić" + }, + "SELECT_GROUP": { + "TITLE": "Ustaw grupę", + "DESCRIPTION": "Jeśli chcesz uruchomić tylko dla grupy zdarzeń, ustaw grupę tutaj" + }, + "SELECT_EVENT": { + "TITLE": "Wybierz zdarzenie", + "DESCRIPTION": "Jeśli chcesz uruchomić tylko dla określonego zdarzenia, określ je tutaj" + } + }, + "TARGET": { + "DESCRIPTION": "Możesz wybrać uruchomienie celu lub uruchomienie go na tych samych warunkach co inne cele.", + "TARGET": { + "DESCRIPTION": "Cel, który chcesz uruchomić dla tej akcji" + }, + "CONDITIONS": { + "DESCRIPTION": "Warunki wykonania" + } + } + }, + "TABLE": { + "CONDITION": "Warunek", + "TYPE": "Typ", + "TARGET": "Cel", + "CREATIONDATE": "Data utworzenia" + } + }, + "TARGET": { + "TITLE": "Cele", + "DESCRIPTION": "Celem jest miejsce docelowe kodu, który chcesz uruchomić z akcji. Utwórz cel tutaj i dodaj go do swoich akcji.", + "CREATE": { + "TITLE": "Utwórz swój cel", + "DESCRIPTION": "Utwórz własny cel poza Zitadel", + "NAME": "Nazwa", + "NAME_DESCRIPTION": "Nadaj swojemu celowi jasną, opisową nazwę, aby ułatwić jego późniejszą identyfikację", + "TYPE": "Typ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Wywołanie REST", + "restAsync": "REST Asynchroniczny" + }, + "TYPES_DESCRIPTION": "Webhook, wywołanie obsługuje kod stanu, ale odpowiedź jest nieistotna\nCall, wywołanie obsługuje kod stanu i odpowiedź\nAsync, wywołanie nie obsługuje ani kodu stanu, ani odpowiedzi, ale może być wywoływane równolegle z innymi celami", + "ENDPOINT": "Punkt końcowy", + "ENDPOINT_DESCRIPTION": "Wprowadź punkt końcowy, w którym hostowany jest Twój kod. Upewnij się, że jest dla nas dostępny!", + "TIMEOUT": "Limit czasu", + "TIMEOUT_DESCRIPTION": "Ustaw maksymalny czas, w jakim cel musi odpowiedzieć. Jeśli zajmie to więcej czasu, zatrzymamy żądanie.", + "INTERRUPT_ON_ERROR": "Przerwij w przypadku błędu", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Zatrzymaj wszystkie wykonania, gdy cele zwrócą błąd", + "INTERRUPT_ON_ERROR_WARNING": "Uwaga: „Przerwij w przypadku błędu” zatrzymuje operacje w przypadku błędu, co grozi zablokowaniem. Przetestuj przy wyłączonej opcji, aby uniknąć blokowania logowania/tworzenia.", + "AWAIT_RESPONSE": "Oczekuj na odpowiedź", + "AWAIT_RESPONSE_DESCRIPTION": "Przed wykonaniem jakichkolwiek innych czynności poczekamy na odpowiedź. Przydatne, jeśli zamierzasz użyć wielu celów dla jednej akcji" + }, + "TABLE": { + "NAME": "Nazwa", + "ENDPOINT": "Punkt końcowy", + "CREATIONDATE": "Data utworzenia", + "REORDER": "Zmień kolejność" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Ma kontrolę nad całą instancją, włącznie z wszystkimi organizacjami", "IAM_OWNER_VIEWER": "Ma uprawnienie do przeglądania całej instancji, włącznie z wszystkimi organizacjami", @@ -789,7 +928,10 @@ "PHONESECTION": "Numery telefonów", "PASSWORDSECTION": "Hasło początkowe", "ADDRESSANDPHONESECTION": "Numer telefonu", - "INITMAILDESCRIPTION": "Jeśli zaznaczone są obie opcje, nie zostanie wysłany żaden e-mail inicjujący. Jeśli zaznaczona jest tylko jedna opcja, zostanie wysłany e-mail, aby udostępnić/zweryfikować dane." + "INITMAILDESCRIPTION": "Jeśli zaznaczone są obie opcje, nie zostanie wysłany żaden e-mail inicjujący. Jeśli zaznaczona jest tylko jedna opcja, zostanie wysłany e-mail, aby udostępnić/zweryfikować dane.", + "SETUPAUTHENTICATIONLATER": "Skonfiguruj uwierzytelnianie później dla tego użytkownika.", + "INVITATION": "Wyślij e-mail zaproszeniowy do konfiguracji uwierzytelniania i weryfikacji e-maila.", + "INITIALPASSWORD": "Ustaw początkowe hasło dla użytkownika." }, "CODEDIALOG": { "TITLE": "Weryfikuj numer telefonu", @@ -1353,6 +1495,7 @@ "BRANDING": "Marka", "PRIVACYPOLICY": "Polityka prywatności", "OIDC": "Czas trwania tokenów OIDC i wygaśnięcie", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Wygląd sekretów", "SECURITY": "Ustawienia bezpieczeństwa", "EVENTS": "Zdarzenia", @@ -1368,7 +1511,8 @@ "APPEARANCE": "Wygląd", "OTHER": "Inne", "STORAGE": "składowanie" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { @@ -1398,7 +1542,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1489,6 +1634,16 @@ "ACTIONS_DESCRIPTION": "Akcje v2 umożliwiają zarządzanie wykonaniami danych i celami. Jeżeli flaga jest włączona, będziesz mógł korzystać z nowego interfejsu API i jego funkcji.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Zakończenie sesji", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Jeśli flaga jest włączona, będziesz mógł zakończyć pojedynczą sesję z interfejsu użytkownika logowania, podając id_token z roszczeniem `sid` jako id_token_hint w punkcie końcowym end_session. Należy pamiętać, że obecnie wszystkie sesje z tego samego agenta użytkownika (przeglądarki) są kończone w interfejsie użytkownika logowania. Sesje zarządzane za pomocą interfejsu API sesji już pozwalają na zakończenie pojedynczych sesji.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Błąd nadrzędny", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Jeśli flaga jest włączona, błąd nadrzędny OIDC zostanie zarejestrowany w konsoli.", + "DISABLEUSERTOKENEVENT": "Wyłącz zdarzenie tokena użytkownika", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Włącz Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout implementuje OpenID Connect Back-Channel Logout 1.0 i może być używany do powiadamiania klientów o zakończeniu sesji u dostawcy OpenID.", + "PERMISSIONCHECKV2": "Sprawdzanie uprawnień V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Jeśli flaga jest włączona, będziesz mógł korzystać z nowego API i jego funkcji.", + "WEBKEY": "Klucz Web", + "WEBKEY_DESCRIPTION": "Jeśli flaga jest włączona, będziesz mógł korzystać z nowego API i jego funkcji.", "STATES": { "INHERITED": "Dziedziczony", "ENABLED": "Włączony", @@ -1501,7 +1656,10 @@ }, "RESET": "Ustaw wszystko na dziedziczone", "CONSOLEUSEV2USERAPI": "Użyj API V2 w konsoli do tworzenia użytkowników", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Gdy ta flaga jest włączona, konsola używa API V2 User do tworzenia nowych użytkowników. W przypadku API V2 nowo utworzeni użytkownicy rozpoczynają bez stanu początkowego." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Gdy ta flaga jest włączona, konsola używa API V2 User do tworzenia nowych użytkowników. W przypadku API V2 nowo utworzeni użytkownicy rozpoczynają bez stanu początkowego.", + "LOGINV2": "Logowanie V2", + "LOGINV2_DESCRIPTION": "Włączenie tej opcji aktywuje nowy interfejs logowania oparty na TypeScript z ulepszonym bezpieczeństwem, wydajnością i możliwością dostosowania.", + "LOGINV2_BASEURI": "Podstawowy URI" }, "DIALOG": { "RESET": { @@ -1638,7 +1796,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Weryfikacja adresu e-mail zakończona", @@ -2588,7 +2748,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Dodaj managera", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index e0def7d2ef..08181f6ead 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Fluxos", "DESCRIPTION": "Escolha um fluxo de autenticação e acione sua ação em um evento específico dentro desse fluxo." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, uma nova e melhorada versão de Actions, já está disponível. A versão atual ainda é acessível, mas o nosso desenvolvimento futuro se concentrará na nova versão, que acabará por substituir a atual." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "A vida útil do token de atualização inativo é o tempo máximo que um token de atualização pode ficar sem uso." } }, + "WEB_KEYS": { + "DESCRIPTION": "Gerencie suas Chaves Web OIDC para assinar e validar tokens com segurança em sua instância do ZITADEL.", + "TABLE": { + "TITLE": "Chaves Web Ativas e Futuras", + "DESCRIPTION": "Suas chaves web ativas e futuras. Ativar uma nova chave desativará a atual.", + "NOTE": "Nota: O endpoint JWKs OIDC retorna uma resposta que pode ser armazenada em cache (padrão: 5 min). Evite ativar uma chave muito cedo, pois ela pode ainda não estar disponível no cache e para os clientes.", + "ACTIVATE": "Ativar próxima Chave Web", + "ACTIVE": "Atualmente ativa", + "NEXT": "Próxima na fila", + "FUTURE": "Futuro", + "WARNING": "A chave da Web tem menos de 5 minutos" + }, + "CREATE": { + "TITLE": "Criar nova Chave Web", + "DESCRIPTION": "Criar uma nova chave web a adicionará à sua lista. O ZITADEL usa, por padrão, chaves RSA2048 com um algoritmo de hash SHA256.", + "KEY_TYPE": "Tipo de Chave", + "BITS": "Bits", + "HASHER": "Algoritmo de Hash", + "CURVE": "Curva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Chaves Web Anteriores", + "DESCRIPTION": "Estas são suas chaves web anteriores que não estão mais ativas.", + "DEACTIVATED_ON": "Desativada em" + } + }, "MESSAGE_TEXTS": { "TITLE": "Textos de Mensagens", "DESCRIPTION": "Personalize os textos do seu e-mail de notificação ou mensagens SMS. Se desejar desativar alguns idiomas, restrinja-os nas configurações de idioma da sua instância.", @@ -502,6 +529,118 @@ "DOWNLOAD": "Baixar", "APPLY": "Aplicar" }, + "ACTIONSTWO": { + "BETA_NOTE": "Você está atualmente usando a nova Actions V2, que está em versão beta. A versão anterior 1 ainda está disponível, mas será descontinuada no futuro. Por favor, reporte quaisquer problemas ou envie feedback.", + "EXECUTION": { + "TITLE": "Ações", + "DESCRIPTION": "As ações permitem que você execute código personalizado em resposta a solicitações de API, eventos ou funções específicas. Use-as para estender o Zitadel, automatizar fluxos de trabalho e integrar-se a outros sistemas.", + "TYPES": { + "request": "Solicitação", + "response": "Resposta", + "event": "Eventos", + "function": "Função" + }, + "DIALOG": { + "CREATE_TITLE": "Criar uma Ação", + "UPDATE_TITLE": "Atualizar uma Ação", + "TYPE": { + "DESCRIPTION": "Selecione quando você deseja que esta Ação seja executada", + "REQUEST": { + "TITLE": "Solicitação", + "DESCRIPTION": "Solicitações que ocorrem dentro do Zitadel. Isso pode ser algo como uma chamada de solicitação de login." + }, + "RESPONSE": { + "TITLE": "Resposta", + "DESCRIPTION": "Uma resposta de uma solicitação dentro do Zitadel. Pense na resposta que você recebe ao buscar um usuário." + }, + "EVENTS": { + "TITLE": "Eventos", + "DESCRIPTION": "Eventos que acontecem dentro do Zitadel. Isso pode ser qualquer coisa, como um usuário criando uma conta, um login bem-sucedido, etc." + }, + "FUNCTIONS": { + "TITLE": "Funções", + "DESCRIPTION": "Funções que você pode chamar dentro do Zitadel. Isso pode ser qualquer coisa, desde enviar um e-mail até criar um usuário." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Escolha se esta ação se aplica a todas as solicitações, um serviço específico (por exemplo, gerenciamento de usuários) ou uma única solicitação (por exemplo, criar usuário).", + "ALL": { + "TITLE": "Todas", + "DESCRIPTION": "Selecione isso se você quiser executar sua ação em cada solicitação" + }, + "ALL_EVENTS": "Selecione isto se quiser executar sua ação em cada evento", + "SELECT_SERVICE": { + "TITLE": "Selecionar Serviço", + "DESCRIPTION": "Escolha um Serviço Zitadel para sua ação." + }, + "SELECT_METHOD": { + "TITLE": "Selecionar Método", + "DESCRIPTION": "Se você quiser executar apenas em uma solicitação específica, selecione-a aqui", + "NOTE": "Se você não selecionar um método, sua ação será executada em todas as solicitações em seu serviço selecionado." + }, + "FUNCTIONNAME": { + "TITLE": "Nome da Função", + "DESCRIPTION": "Escolha a função que você deseja executar" + }, + "SELECT_GROUP": { + "TITLE": "Definir Grupo", + "DESCRIPTION": "Se você quiser executar apenas em um grupo de eventos, defina o grupo aqui" + }, + "SELECT_EVENT": { + "TITLE": "Selecionar Evento", + "DESCRIPTION": "Se você quiser executar apenas em um evento específico, especifique-o aqui" + } + }, + "TARGET": { + "DESCRIPTION": "Você pode escolher executar um destino ou executá-lo nas mesmas condições que outros destinos.", + "TARGET": { + "DESCRIPTION": "O destino que você deseja executar para esta ação" + }, + "CONDITIONS": { + "DESCRIPTION": "Condições de Execução" + } + } + }, + "TABLE": { + "CONDITION": "Condição", + "TYPE": "Tipo", + "TARGET": "Destino", + "CREATIONDATE": "Data de Criação" + } + }, + "TARGET": { + "TITLE": "Destinos", + "DESCRIPTION": "Um destino é o destino do código que você deseja executar a partir de uma ação. Crie um destino aqui e adicione-o às suas ações.", + "CREATE": { + "TITLE": "Criar seu Destino", + "DESCRIPTION": "Crie seu próprio destino fora do Zitadel", + "NAME": "Nome", + "NAME_DESCRIPTION": "Dê ao seu destino um nome claro e descritivo para torná-lo fácil de identificar mais tarde", + "TYPE": "Tipo", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Chamada REST", + "restAsync": "REST Assíncrono" + }, + "TYPES_DESCRIPTION": "Webhook, a chamada lida com o código de status, mas a resposta é irrelevante\nCall, a chamada lida com o código de status e a resposta\nAsync, a chamada não lida nem com o código de status nem com a resposta, mas pode ser chamada em paralelo com outros alvos", + "ENDPOINT": "Ponto de Extremidade", + "ENDPOINT_DESCRIPTION": "Insira o ponto de extremidade onde seu código está hospedado. Certifique-se de que ele esteja acessível para nós!", + "TIMEOUT": "Tempo Limite", + "TIMEOUT_DESCRIPTION": "Defina o tempo máximo que seu destino tem para responder. Se demorar mais, interromperemos a solicitação.", + "INTERRUPT_ON_ERROR": "Interromper em Caso de Erro", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Pare todas as execuções quando os destinos retornarem um erro", + "INTERRUPT_ON_ERROR_WARNING": "Atenção: “Interromper em caso de erro” interrompe as operações em caso de falha, com risco de bloqueio. Teste com esta opção desativada para evitar bloquear o login/criação.", + "AWAIT_RESPONSE": "Aguardar Resposta", + "AWAIT_RESPONSE_DESCRIPTION": "Aguardaremos uma resposta antes de fazermos qualquer outra coisa. Útil se você pretende usar vários destinos para uma única ação" + }, + "TABLE": { + "NAME": "Nome", + "ENDPOINT": "Ponto de Extremidade", + "CREATIONDATE": "Data de Criação", + "REORDER": "Reordenar" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Tem controle sobre toda a instância, incluindo todas as organizações", "IAM_OWNER_VIEWER": "Tem permissão para revisar toda a instância, incluindo todas as organizações", @@ -790,7 +929,10 @@ "PHONESECTION": "Números de Telefone", "PASSWORDSECTION": "Senha Inicial", "ADDRESSANDPHONESECTION": "Número de telefone", - "INITMAILDESCRIPTION": "Se ambas as opções forem selecionadas, nenhum e-mail de inicialização será enviado. Se apenas uma das opções for selecionada, um e-mail para fornecer/verificar os dados será enviado." + "INITMAILDESCRIPTION": "Se ambas as opções forem selecionadas, nenhum e-mail de inicialização será enviado. Se apenas uma das opções for selecionada, um e-mail para fornecer/verificar os dados será enviado.", + "SETUPAUTHENTICATIONLATER": "Configurar autenticação mais tarde para este usuário.", + "INVITATION": "Enviar um E-mail de convite para configuração de autenticação e verificação de E-mail.", + "INITIALPASSWORD": "Defina uma senha inicial para o usuário." }, "CODEDIALOG": { "TITLE": "Verificar Número de Telefone", @@ -1355,6 +1497,7 @@ "BRANDING": "Marca", "PRIVACYPOLICY": "Política de Privacidade", "OIDC": "Tempo de Vida e Expiração do Token OIDC", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Aparência de Segredo", "SECURITY": "Configurações de Segurança", "EVENTS": "Eventos", @@ -1370,7 +1513,8 @@ "APPEARANCE": "Aparência", "OTHER": "Outro", "STORAGE": "armazenar" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { @@ -1400,7 +1544,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1491,6 +1636,16 @@ "ACTIONS_DESCRIPTION": "Actions v2 permitem gerenciar execuções e destinos de dados. Se a flag estiver habilitada, você poderá usar a nova API e seus recursos.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Término de sessão", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Se a bandeira estiver habilitada, você poderá encerrar uma sessão única da interface do usuário de login fornecendo um id_token com uma reivindicação `sid como id_token_hint no ponto final de end_session. Observe que atualmente todas as sessões do mesmo agente de usuário (navegador) são encerradas na interface do usuário de login. As sessões gerenciadas por meio da API de sessão já permitem o encerramento de sessões individuais.", + "DEBUGOIDCPARENTERROR": "Erro de Depuração do Pai OIDC", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Se a bandeira estiver ativada, o erro do pai OIDC será registrado no console.", + "DISABLEUSERTOKENEVENT": "Desativar Evento de Token de Usuário", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Ativar Logout de Backchannel", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "O Logout de Back-Channel implementa o OpenID Connect Back-Channel Logout 1.0 e pode ser usado para notificar os clientes sobre a terminação da sessão no Provedor de OpenID.", + "PERMISSIONCHECKV2": "Verificação de Permissão V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Se a bandeira estiver ativada, você poderá usar a nova API e seus recursos.", + "WEBKEY": "Chave Web", + "WEBKEY_DESCRIPTION": "Se a bandeira estiver ativada, você poderá usar a nova API e seus recursos.", "STATES": { "INHERITED": "Herdade", "ENABLED": "Habilitado", @@ -1503,7 +1658,10 @@ }, "RESET": "Definir tudo para herdar", "CONSOLEUSEV2USERAPI": "Use a API V2 no console para criação de usuários", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Quando esta opção está ativada, o console utiliza a API V2 de Usuários para criar novos usuários. Com a API V2, os novos usuários criados começam sem um estado inicial." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Quando esta opção está ativada, o console utiliza a API V2 de Usuários para criar novos usuários. Com a API V2, os novos usuários criados começam sem um estado inicial.", + "LOGINV2": "Login V2", + "LOGINV2_DESCRIPTION": "Ativar esta opção ativa a nova interface de login baseada em TypeScript, com melhorias na segurança, desempenho e personalização.", + "LOGINV2_BASEURI": "URI base" }, "DIALOG": { "RESET": { @@ -1640,7 +1798,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Verificação de email concluída", @@ -2584,7 +2744,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Adicionar um Gerente", diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json new file mode 100644 index 0000000000..b07897f316 --- /dev/null +++ b/console/src/assets/i18n/ro.json @@ -0,0 +1,2851 @@ +{ + "APP_NAME": "ZITADEL", + "DESCRIPTIONS": { + "METADATA_TITLE": "Metadata", + "HOME": { + "TITLE": "Începeți cu ZITADEL", + "NEXT": { + "TITLE": "Pașii următori", + "DESCRIPTION": "Finalizați următorii pași pentru a vă securiza aplicația.", + "CREATE_PROJECT": { + "TITLE": "Creați un proiect", + "DESCRIPTION": "Adăugați un proiect și definiți rolurile și autorizațiile acestuia." + } + }, + "MORE_SHORTCUTS": { + "GET_STARTED": { + "TITLE": "Începeți", + "DESCRIPTION": "Urmați ghidul rapid pas cu pas și începeți să construiți imediat." + }, + "DOCS": { + "TITLE": "Documentație", + "DESCRIPTION": "Explorați baza de cunoștințe ZITADEL pentru a vă familiariza cu conceptele și ideile de bază. Aflați cum funcționează ZITADEL și cum să îl utilizați." + }, + "EXAMPLES": { + "TITLE": "Exemple și Kituri de Dezvoltare Software", + "DESCRIPTION": "Navigați prin exemplele și SDK-urile noastre pentru a utiliza ZITADEL împreună cu limbajele și instrumentele de programare preferate." + } + } + }, + "ORG": { + "TITLE": "Organizație", + "DESCRIPTION": "O organizație găzduiește utilizatori, proiecte cu aplicații, furnizori de identitate și setări precum brandingul companiei. Doriți să partajați setări între mai multe organizații? Configurați setările implicite.", + "METADATA": "Adăugați atribute personalizate organizației, cum ar fi locația sau un identificator într-un alt sistem. Puteți utiliza aceste informații în acțiunile dvs." + }, + "PROJECTS": { + "TITLE": "Proiecte", + "DESCRIPTION": "Un proiect găzduiește una sau mai multe aplicații, pe care le puteți utiliza pentru a vă autentifica utilizatorii. De asemenea, vă puteți autoriza utilizatorii cu proiecte. Pentru a permite utilizatorilor din alte organizații să se conecteze la aplicațiile dvs., acordați-le acces la proiectul dvs.

Dacă nu găsiți un proiect, contactați proprietarul proiectelor sau pe cineva cu drepturile corespunzătoare pentru a obține acces.", + "OWNED": { + "TITLE": "Proiecte deținute", + "DESCRIPTION": "Acestea sunt proiectele pe care le dețineți. Puteți gestiona setările acestor proiecte, autorizațiile și aplicațiile." + }, + "GRANTED": { + "TITLE": "Proiecte acordate", + "DESCRIPTION": "Acestea sunt proiectele pe care alte organizații vi le-au acordat. Cu proiectele acordate, puteți oferi utilizatorilor dvs. acces la aplicațiile altor organizații." + } + }, + "USERS": { + "TITLE": "Utilizatori", + "DESCRIPTION": "Un utilizator este o persoană sau o mașină care vă poate accesa aplicațiile.", + "HUMANS": { + "TITLE": "Utilizatori", + "DESCRIPTION": "Utilizatorii se autentifică interactiv într-o sesiune de browser cu o solicitare de autentificare.", + "METADATA": "Adăugați atribute personalizate utilizatorului, cum ar fi departamentul. Puteți utiliza aceste informații în acțiunile dvs." + }, + "MACHINES": { + "TITLE": "Utilizatori de servicii", + "DESCRIPTION": "Utilizatorii de servicii se autentifică non-interactiv folosind un token JWT bearer semnat cu o cheie privată. De asemenea, pot utiliza un token de acces personal.", + "METADATA": "Adăugați atribute personalizate utilizatorului, cum ar fi sistemul de autentificare. Puteți utiliza aceste informații în acțiunile dvs." + }, + "SELF": { + "METADATA": "Adăugați atribute personalizate utilizatorului dvs., cum ar fi departamentul. Puteți utiliza aceste informații în acțiunile organizațiilor dvs." + } + }, + "AUTHORIZATIONS": { + "TITLE": "Autorizații", + "DESCRIPTION": "Autorizațiile definesc drepturile de acces ale unui utilizator la un proiect. Puteți acorda unui utilizator acces la un proiect și puteți defini rolurile utilizatorilor în cadrul acelui proiect." + }, + "ACTIONS": { + "TITLE": "Acțiuni", + "DESCRIPTION": "Executați cod personalizat pe evenimente care se întâmplă pe măsură ce utilizatorii dvs. se autentifică la ZITADEL. Automatizați-vă procesele, îmbogățiți metadatele utilizatorilor și tokenurile lor sau notificați sistemele externe.", + "SCRIPTS": { + "TITLE": "Scripturi", + "DESCRIPTION": "Scrieți codul JavaScript o singură dată și declanșați-l în mai multe fluxuri." + }, + "FLOWS": { + "TITLE": "Fluxuri", + "DESCRIPTION": "Alegeți un flux de autentificare și declanșați acțiunea dvs. la un anumit eveniment din cadrul acestui flux." + }, + "ACTIONSTWO_NOTE": "Actions V2, o nouă versiune îmbunătățită a Actions, este acum disponibilă. Versiunea actuală este încă accesibilă, dar dezvoltarea viitoare se va concentra pe cea nouă, care în cele din urmă va înlocui versiunea actuală." + }, + "SETTINGS": { + "INSTANCE": { + "TITLE": "Setări implicite", + "DESCRIPTION": "Setări implicite pentru toate organizațiile. Cu permisiunile potrivite, unele dintre ele pot fi suprascrise în setările organizației." + }, + "ORG": { + "TITLE": "Setări organizație", + "DESCRIPTION": "Personalizați setările organizației dvs." + }, + "FEATURES": { + "TITLE": "Setări caracteristici", + "DESCRIPTION": "Deblocați caracteristici pentru instanța dvs." + }, + "IDPS": { + "TITLE": "Furnizori de identitate", + "DESCRIPTION": "Creați și activați furnizori de identitate externi. Alegeți un furnizor bine-cunoscut sau configurați orice alt furnizor compatibil OIDC, OAuth sau SAML la alegere. Puteți chiar să utilizați tokenurile JWT existente ca identități federate prin configurarea unui furnizor de identitate JWT.", + "NEXT": "Ce urmează?", + "SAML": { + "TITLE": "Configurați-vă furnizorul de identitate SAML", + "DESCRIPTION": "ZITADEL este configurat. Acum, furnizorul dvs. de identitate SAML are nevoie de o anumită configurație. Majoritatea furnizorilor vă permit să încărcați pur și simplu întregul XML de metadate ZITADEL. Alți furnizori vă cer să furnizați doar câteva URL-uri distincte, cum ar fi, de exemplu, ID-ul entității (URL-ul metadatelor), URL-ul Assertion Consumer Service (ACS) sau URL-ul Single Logout." + }, + "CALLBACK": { + "TITLE": "Configurați-vă furnizorul de identitate {{ provider }}", + "DESCRIPTION": "Înainte de a putea configura ZITADEL, transmiteți acest URL furnizorului dvs. de identitate pentru a activa redirecționarea browserului înapoi la ZITADEL după autentificare." + }, + "JWT": { + "TITLE": "Utilizați JWT-urile ca identități federate", + "DESCRIPTION": "Furnizorul de identitate JWT vă permite să utilizați tokenurile JWT existente ca identități federate. Această caracteristică este utilă dacă aveți deja un emitent pentru JWT-uri. Cu un IdP JWT, puteți utiliza aceste JWT-uri pentru a crea și actualiza utilizatori în ZITADEL din mers." + }, + "LDAP": { + "TITLE": "Configurați ZITADEL pentru a vă conecta la furnizorul dvs. de identitate LDAP", + "DESCRIPTION": "Furnizați detaliile de conectare la serverul dvs. LDAP și configurați maparea atributelor LDAP la atributele ZITADEL." + }, + "AUTOFILL": { + "TITLE": "Completare automată a datelor utilizatorului", + "DESCRIPTION": "Utilizați o acțiune pentru a îmbunătăți experiența utilizatorilor dvs. Puteți pre-completa formularul de înregistrare ZITADEL cu valori de la furnizorul de identitate." + }, + "ACTIVATE": { + "TITLE": "Activați IdP-ul", + "DESCRIPTION": "IdP-ul dvs. nu este încă activ. Activați-l pentru a permite utilizatorilor să se conecteze." + } + }, + "PW_COMPLEXITY": { + "TITLE": "Complexitatea parolei", + "DESCRIPTION": "Asigurați-vă că utilizatorii dvs. utilizează parole puternice definind reguli de complexitate." + }, + "BRANDING": { + "TITLE": "Branding", + "DESCRIPTION": "Personalizați aspectul formularului dvs. de autentificare. Nu uitați să aplicați configurația dvs. când ați terminat." + }, + "PRIVACY_POLICY": { + "TITLE": "Linkuri externe", + "DESCRIPTION": "Ghidați utilizatorii către resurse externe personalizate afișate pe pagina de autentificare. Utilizatorii trebuie să accepte Termenii și condițiile și Politica de confidențialitate înainte de a se putea înregistra. Modificați linkul către documentația dvs. sau setați un șir gol pentru a ascunde butonul de documentație din consolă. Adăugați un link extern personalizat și un text personalizat pentru acel link în consolă sau setați-le goale pentru a ascunde acel buton." + }, + "SMTP_PROVIDER": { + "TITLE": "Setări SMTP", + "DESCRIPTION": "Configurați serverul dvs. SMTP pentru a utiliza un domeniu pentru adresa expeditorului pe care utilizatorii dvs. o cunosc și au încredere." + }, + "SMS_PROVIDER": { + "TITLE": "Setări SMS", + "DESCRIPTION": "Pentru a debloca toate caracteristicile ZITADEL, configurați Twilio pentru a trimite mesaje SMS utilizatorilor dvs." + }, + "IAM_EVENTS": { + "TITLE": "Evenimente", + "DESCRIPTION": "Această pagină afișează toate modificările de stare din instanța dvs., până la limita de urmărire de audit a instanțelor dvs. Filtrați lista după intervalul de timp în scopuri de depanare sau filtrați-o după un agregat în scopuri de audit." + }, + "IAM_FAILED_EVENTS": { + "TITLE": "Evenimente eșuate", + "DESCRIPTION": "Această pagină afișează toate evenimentele eșuate din instanța dvs. Dacă ZITADEL nu se comportă așa cum vă așteptați, verificați întotdeauna mai întâi această listă." + }, + "IAM_VIEWS": { + "TITLE": "Vizualizări", + "DESCRIPTION": "Această pagină afișează toate vizualizările bazei dvs. de date și când au procesat cel mai recent eveniment. Dacă vă lipsesc date, verificați dacă vizualizarea este actualizată." + }, + "LANGUAGES": { + "TITLE": "Limbi", + "DESCRIPTION": "Restricționați limbile în care formularul de autentificare și mesajele de notificare sunt traduse. Dacă doriți să dezactivați unele dintre limbi, trageți-le în secțiunea Limbi nepermise. Puteți specifica o limbă permisă ca limbă implicită. Dacă limba preferată a utilizatorilor nu este permisă, se utilizează limba implicită." + }, + "SECRET_GENERATORS": { + "TITLE": "Generatoare de secrete", + "DESCRIPTION": "Definiți complexitățile și duratele de viață ale secretelor dvs. O complexitate și o durată de viață mai mari îmbunătățesc securitatea, o complexitate și o durată de viață mai mici îmbunătățesc performanța de decriptare." + }, + "SECURITY": { + "TITLE": "Setări de securitate", + "DESCRIPTION": "Activați caracteristicile ZITADEL care pot avea impact asupra securității. Ar trebui să știți cu adevărat ce faceți înainte de a modifica aceste setări." + }, + "OIDC": { + "TITLE": "Setări OpenID Connect", + "DESCRIPTION": "Configurați duratele de viață ale tokenurilor OIDC. Utilizați durate de viață mai scurte pentru a crește securitatea utilizatorilor dvs., utilizați durate de viață mai lungi pentru a crește confortul utilizatorilor dvs.", + "LABEL_HOURS": "Durata maximă de viață în ore", + "LABEL_DAYS": "Durata maximă de viață în zile", + "ACCESS_TOKEN": { + "TITLE": "Token de acces", + "DESCRIPTION": "Tokenul de acces este utilizat pentru a autentifica un utilizator. Este un token de scurtă durată care este utilizat pentru a accesa datele utilizatorului. Utilizați o durată de viață scurtă pentru a minimiza riscul de acces neautorizat. Tokenurile de acces pot fi reîmprospătate automat utilizând un token de reîmprospătare." + }, + "ID_TOKEN": { + "TITLE": "Token ID", + "DESCRIPTION": "Tokenul ID este un JSON Web Token (JWT) care conține informații despre utilizator. Durata de viață a tokenului ID nu trebuie să depășească durata de viață a tokenului de acces." + }, + "REFRESH_TOKEN": { + "TITLE": "Token de reîmprospătare", + "DESCRIPTION": "Tokenul de reîmprospătare este utilizat pentru a obține un token de acces nou. Este un token de lungă durată care este utilizat pentru a reîmprospăta tokenul de acces. Un utilizator trebuie să se re-autentifice manual când tokenul de reîmprospătare expiră." + }, + "REFRESH_TOKEN_IDLE": { + "TITLE": "Token de reîmprospătare inactiv", + "DESCRIPTION": "Durata de viață inactivă a tokenului de reîmprospătare este timpul maxim în care un token de reîmprospătare poate fi neutilizat." + } + }, + "WEB_KEYS": { + "DESCRIPTION": "Gestionează-ți cheile web OIDC pentru a semna și valida în siguranță tokenurile pentru instanța ta ZITADEL.", + "TABLE": { + "TITLE": "Chei Web Active și Viitoare", + "DESCRIPTION": "Cheile tale web active și viitoare. Activarea unei noi chei va dezactiva cheia curentă.", + "NOTE": "Notă: Endpoint-ul JWKs OIDC returnează un răspuns care poate fi stocat în cache (implicit 5 min). Evită activarea unei chei prea devreme, deoarece este posibil să nu fie încă disponibilă în cache și pentru clienți.", + "ACTIVATE": "Activează următoarea Cheie Web", + "ACTIVE": "În prezent activă", + "NEXT": "Următoarea în coadă", + "FUTURE": "Viitoare", + "WARNING": "Cheia web are mai puțin de 5 minute" + }, + "CREATE": { + "TITLE": "Creează o nouă Cheie Web", + "DESCRIPTION": "Crearea unei noi chei web o va adăuga pe lista ta. ZITADEL folosește implicit chei RSA2048 cu un algoritm de hash SHA256.", + "KEY_TYPE": "Tip de Cheie", + "BITS": "Biti", + "HASHER": "Algoritm de Hash", + "CURVE": "Curbă" + }, + "PREVIOUS_TABLE": { + "TITLE": "Chei Web Anterioare", + "DESCRIPTION": "Acestea sunt cheile tale web anterioare care nu mai sunt active.", + "DEACTIVATED_ON": "Dezactivată pe" + } + }, + "MESSAGE_TEXTS": { + "TITLE": "Texte de mesaje", + "DESCRIPTION": "Personalizați textele mesajelor de e-mail sau SMS de notificare. Dacă doriți să dezactivați unele dintre limbi, restricționați-le în setările de limbă ale instanțelor dvs.", + "TYPE_DESCRIPTIONS": { + "DC": "Când revendicați un domeniu pentru organizația dvs., utilizatorii care nu utilizează acest domeniu în numele lor de conectare vor fi solicitați să își schimbe numele de conectare pentru a se potrivi cu domeniul revendicat.", + "INIT": "Când un utilizator este creat, acesta va primi un e-mail cu un link pentru a-și seta parola.", + "PC": "Când un utilizator își schimbă parola, acesta va primi o notificare despre modificare dacă ați activat acest lucru în setările de notificare.", + "PL": "Când un utilizator adaugă o metodă de autentificare fără parolă, acesta trebuie să o activeze făcând clic pe un link dintr-un e-mail.", + "PR": "Când un utilizator își resetează parola, acesta va primi un e-mail cu un link pentru a seta o parolă nouă.", + "VE": "Când un utilizator își schimbă adresa de e-mail, acesta va primi un e-mail cu un link pentru a verifica noua adresă.", + "VP": "Când un utilizator își schimbă numărul de telefon, acesta va primi un SMS cu un cod pentru a verifica noul număr.", + "VEO": "Când un utilizator adaugă o parolă unică prin metoda e-mail, acesta trebuie să o activeze introducând un cod trimis la adresa sa de e-mail.", + "VSO": "Când un utilizator adaugă o parolă unică prin metoda SMS, acesta trebuie să o activeze introducând un cod trimis la numărul său de telefon.", + "IU": "Când este creat un cod de invitație pentru utilizator, acesta va primi un e-mail cu un link pentru a-și seta metoda de autentificare." + } + }, + "LOGIN_TEXTS": { + "TITLE": "Texte interfață de autentificare", + "DESCRIPTION": "Personalizați textele formularului dvs. de autentificare. Dacă un text este gol, substituentul afișează valoarea implicită. Dacă doriți să dezactivați unele dintre limbi, restricționați-le în setările de limbă ale instanțelor dvs." + }, + "DOMAINS": { + "TITLE": "Setări domeniu", + "DESCRIPTION": "Definiți restricții asupra domeniilor dvs. și configurați modelele de nume de conectare.", + "REQUIRE_VERIFICATION": { + "TITLE": "Solicitați ca domeniile personalizate să fie verificate", + "DESCRIPTION": "Dacă acest lucru este activat, domeniile organizației trebuie să fie verificate înainte de a putea fi utilizate pentru descoperirea domeniului sau sufixarea numelui de utilizator." + }, + "LOGIN_NAME_PATTERN": { + "TITLE": "Model de nume de conectare", + "DESCRIPTION": "Controlați modelul numelor de conectare ale utilizatorilor dvs. ZITADEL selectează organizația utilizatorilor dvs. imediat ce aceștia își introduc numele de conectare. Prin urmare, numele de conectare trebuie să fie unice în toate organizațiile. Dacă aveți utilizatori care au un cont în mai multe domenii, puteți asigura unicitatea prin sufixarea numelor de conectare cu domeniul organizației." + }, + "DOMAIN_VERIFICATION": { + "TITLE": "Verificarea domeniului", + "DESCRIPTION": "Permiteți organizației dvs. să utilizeze numai domeniile pe care le controlează efectiv. Dacă este activată, domeniile organizației sunt verificate periodic prin DNS sau provocare HTTP înainte de a putea fi utilizate. Aceasta este o caracteristică de securitate pentru a preveni deturnarea domeniului." + }, + "SMTP_SENDER_ADDRESS": { + "TITLE": "Adresa expeditorului SMTP", + "DESCRIPTION": "Permiteți o adresă expeditorului SMTP numai dacă se potrivește cu unul dintre domeniile instanței dvs." + } + }, + "LOGIN": { + "LIFETIMES": { + "TITLE": "Durate de viață autentificare", + "DESCRIPTION": "Consolidați-vă securitatea prin reducerea unor durate maxime de viață legate de autentificare.", + "LABEL": "Durata maximă de viață în ore", + "PW_CHECK": { + "TITLE": "Verificare parolă", + "DESCRIPTION": "Utilizatorii vor trebui să se re-autentifice cu parola lor după această perioadă." + }, + "EXT_LOGIN_CHECK": { + "TITLE": "Verificare autentificare externă", + "DESCRIPTION": "Utilizatorii dvs. sunt redirecționați către furnizorii lor de identitate externi după aceste perioade." + }, + "MULTI_FACTOR_INIT": { + "TITLE": "Verificare inițializare multifactor", + "DESCRIPTION": "Utilizatorii dvs. vor fi solicitați să configureze un al doilea factor sau un Multifactor după aceste perioade, dacă nu au făcut-o deja. O durată de viață de 0 dezactivează această solicitare." + }, + "SECOND_FACTOR_CHECK": { + "TITLE": "Verificare al doilea factor", + "DESCRIPTION": "Utilizatorii dvs. trebuie să își revalideze al doilea factor în aceste perioade." + }, + "MULTI_FACTOR_CHECK": { + "TITLE": "Verificare multifactor", + "DESCRIPTION": "Utilizatorii dvs. trebuie să își revalideze multifactorul în aceste perioade." + } + }, + "FORM": { + "TITLE": "Formular de autentificare", + "DESCRIPTION": "Personalizați formularul de autentificare.", + "USERNAME_PASSWORD_ALLOWED": { + "TITLE": "Nume de utilizator și parolă permise", + "DESCRIPTION": "Permiteți utilizatorilor dvs. să se conecteze cu numele lor de utilizator și parola. Dacă acest lucru este dezactivat, utilizatorii dvs. se pot conecta numai utilizând autentificarea fără parolă sau cu un furnizor de identitate extern." + }, + "USER_REGISTRATION_ALLOWED": { + "TITLE": "Înregistrarea utilizatorilor permisă", + "DESCRIPTION": "Permiteți utilizatorilor anonimi să își creeze un cont." + }, + "ORG_REGISTRATION_ALLOWED": { + "TITLE": "Înregistrarea organizației permisă", + "DESCRIPTION": "Permiteți utilizatorilor anonimi să își creeze o organizație." + }, + "EXTERNAL_LOGIN_ALLOWED": { + "TITLE": "Autentificare externă permisă", + "DESCRIPTION": "Permiteți utilizatorilor dvs. să se conecteze cu un furnizor de identitate extern în loc să utilizeze utilizatorul ZITADEL pentru a se conecta." + }, + "HIDE_PASSWORD_RESET": { + "TITLE": "Resetarea parolei ascunsă", + "DESCRIPTION": "Nu permiteți utilizatorilor dvs. să își reseteze parola." + }, + "DOMAIN_DISCOVERY_ALLOWED": { + "TITLE": "Descoperirea domeniului permisă", + "DESCRIPTION": "Găsiți organizațiile utilizatorilor dvs. în funcție de domeniul numelor lor de conectare, de exemplu, adresa lor de e-mail." + }, + "IGNORE_UNKNOWN_USERNAMES": { + "TITLE": "Ignorați numele de utilizator necunoscute", + "DESCRIPTION": "Dacă acest lucru este activat, formularul de autentificare nu va afișa un mesaj de eroare dacă numele de utilizator este necunoscut. Acest lucru ajută la prevenirea ghicirii numelui de utilizator." + }, + "DISABLE_EMAIL_LOGIN": { + "TITLE": "Dezactivați autentificarea prin e-mail", + "DESCRIPTION": "Dacă acest lucru este activat, utilizatorii dvs. nu își pot utiliza adresele de e-mail pentru a se conecta. Atenție, dacă dezactivați acest lucru, adresele de e-mail ale utilizatorilor dvs. trebuie să fie unice în toate organizațiile pentru a se putea conecta." + }, + "DISABLE_PHONE_LOGIN": { + "TITLE": "Dezactivați autentificarea prin telefon", + "DESCRIPTION": "Dacă acest lucru este activat, utilizatorii dvs. nu își pot utiliza numerele de telefon pentru a se conecta. Atenție, dacă dezactivați acest lucru, numerele de telefon ale utilizatorilor dvs. trebuie să fie unice în toate organizațiile pentru a se putea conecta." + } + } + } + } + }, + "PAGINATOR": { + "PREVIOUS": "Anterior", + "NEXT": "Următorul", + "COUNT": "Rezultate totale", + "MORE": "Mai multe" + }, + "FOOTER": { + "LINKS": { + "CONTACT": "Contact", + "TOS": "Termeni și condiții", + "PP": "Politica de confidențialitate" + }, + "THEME": { + "DARK": "Întunecat", + "LIGHT": "Deschis" + } + }, + "HOME": { + "WELCOME": "Începeți cu ZITADEL", + "DISCLAIMER": "ZITADEL vă tratează datele în mod confidențial și sigur.", + "DISCLAIMERLINK": "Informații suplimentare", + "DOCUMENTATION": { + "DESCRIPTION": "Începeți rapid cu ZITADEL." + }, + "GETSTARTED": { + "DESCRIPTION": "Începeți rapid cu ZITADEL." + }, + "QUICKSTARTS": { + "LABEL": "Primii pași", + "DESCRIPTION": "Începeți rapid cu ZITADEL." + }, + "SHORTCUTS": { + "SHORTCUTS": "Comenzi rapide", + "SETTINGS": "Comenzi rapide disponibile", + "PROJECTS": "Proiecte", + "REORDER": "Țineți apăsat și trageți țigla pentru a o muta", + "ADD": "Țineți apăsat și trageți o țiglă pentru a o adăuga" + } + }, + "ONBOARDING": { + "DESCRIPTION": "Pașii următori", + "MOREDESCRIPTION": "mai multe comenzi rapide", + "COMPLETED": "finalizat", + "DISMISS": "Nu, mulțumesc, sunt profesionist.", + "CARD": { + "TITLE": "Puneți ZITADEL să funcționeze", + "DESCRIPTION": "Această listă de verificare ajută la configurarea instanței și vă ghidează prin cei mai esențiali pași" + }, + "MILESTONES": { + "instance.policy.label.added": { + "title": "Configurați-vă brandul", + "description": "Definiți culoarea și forma autentificării și încărcați logo-ul și pictogramele.", + "action": "Configurați brandingul" + }, + "instance.smtp.config.added": { + "title": "Configurați-vă setările SMTP", + "description": "Setați propriile setări ale serverului de e-mail.", + "action": "Configurați SMTP" + }, + "PROJECT_CREATED": { + "title": "Creați un proiect", + "description": "Adăugați un proiect și definiți rolurile și autorizațiile acestuia.", + "action": "Creați proiect" + }, + "APPLICATION_CREATED": { + "title": "Înregistrați-vă aplicația", + "description": "Înregistrați-vă aplicația web, nativă, api sau saml și configurați un flux de autentificare.", + "action": "Înregistrați aplicația" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "Conectați-vă la aplicația dvs.", + "description": "Integrați aplicația dvs. cu ZITADEL pentru autentificare și testați-o conectându-vă cu utilizatorul dvs. administrator.", + "action": "Conectați-vă" + }, + "user.human.added": { + "title": "Adăugați utilizatori", + "description": "Adăugați utilizatorii aplicației dvs.", + "action": "Adăugați utilizator" + }, + "user.grant.added": { + "title": "Acordați utilizatorilor", + "description": "Permiteți utilizatorilor să acceseze aplicația dvs. și să își configureze rolul.", + "action": "Acordați utilizatorului" + } + } + }, + "MENU": { + "INSTANCE": "Setări implicite", + "DASHBOARD": "Acasă", + "PERSONAL_INFO": "Informații personale", + "DOCUMENTATION": "Documentație", + "INSTANCEOVERVIEW": "Instanță", + "ORGS": "Organizații", + "VIEWS": "Vizualizări", + "EVENTS": "Evenimente", + "FAILEDEVENTS": "Evenimente eșuate", + "ORGANIZATION": "Organizație", + "PROJECT": "Proiecte", + "PROJECTOVERVIEW": "Prezentare generală", + "PROJECTGRANTS": "Granturi", + "ROLES": "Roluri", + "GRANTEDPROJECT": "Proiecte acordate", + "HUMANUSERS": "Utilizatori", + "MACHINEUSERS": "Utilizatori de servicii", + "LOGOUT": "Deconectați toți utilizatorii", + "NEWORG": "Organizație nouă", + "IAMADMIN": "Sunteți administrator IAM. Rețineți că aveți permisiuni extinse.", + "SHOWORGS": "Afișați toate organizațiile", + "GRANTS": "Autorizații", + "ACTIONS": "Acțiuni", + "PRIVACY": "Confidențialitate", + "TOS": "Termeni și condiții", + "OPENSHORTCUTSTOOLTIP": "Tastați ? pentru a afișa comenzile rapide de la tastatură", + "SETTINGS": "Setări", + "CUSTOMERPORTAL": "Portalul clienților" + }, + "QUICKSTART": { + "TITLE": "Integrați ZITADEL în aplicația dvs.", + "DESCRIPTION": "Integrați ZITADEL în aplicația dvs. sau utilizați unul dintre exemplele noastre pentru a începe în câteva minute.", + "BTN_START": "Creați aplicație", + "BTN_LEARNMORE": "Aflați mai multe", + "CREATEPROJECTFORAPP": "Creați proiectul {{value}}", + "SELECT_FRAMEWORK": "Selectați Framework", + "FRAMEWORK": "Framework", + "FRAMEWORK_OTHER": "Altele (OIDC, SAML, API)", + "ALMOSTDONE": "Aproape ați terminat.", + "REVIEWCONFIGURATION": "Revizuiți configurația", + "REVIEWCONFIGURATION_DESCRIPTION": "Am creat o configurație de bază pentru aplicațiile {{value}}. Puteți adapta această configurație nevoilor dvs. după creare.", + "REDIRECTS": "Configurați redirecționări", + "DEVMODEWARN": "Modul Dev este activat în mod implicit. Puteți actualiza valorile pentru producție mai târziu.", + "GUIDE": "Ghid", + "BROWSEEXAMPLES": "Răsfoiți Exemple și SDK-uri", + "DUPLICATEAPPRENAME": "Există deja o aplicație cu același nume. Vă rugăm să alegeți un alt nume.", + "DIALOG": { + "CHANGE": { + "TITLE": "Modificați Framework-ul", + "DESCRIPTION": "Alegeți unul dintre framework-urile disponibile pentru configurarea rapidă a aplicației dvs." + } + } + }, + "ACTIONS": { + "ACTIONS": "Acțiuni", + "FILTER": "Filtru", + "RENAME": "Redenumiți", + "SET": "Setați", + "COPY": "Copiați în clipboard", + "COPIED": "Copiat în clipboard.", + "RESET": "Resetați", + "RESETDEFAULT": "Resetați la implicit", + "RESETTO": "Resetați la:", + "RESETCURRENT": "Resetați la curent", + "SHOW": "Afișați", + "HIDE": "Ascundeți", + "SAVE": "Salvați", + "SAVENOW": "Salvați acum", + "NEW": "Nou", + "ADD": "Adăugați", + "CREATE": "Creați", + "CONTINUE": "Continuați", + "CONTINUEWITH": "Continuați cu {{value}}", + "BACK": "Înapoi", + "CLOSE": "Închideți", + "CLEAR": "Ștergeți", + "CANCEL": "Anulați", + "INFO": "Info", + "OK": "OK", + "SELECT": "Selectați", + "VIEW": "Afișați", + "SELECTIONDELETE": "Ștergeți selecția", + "DELETE": "Ștergeți", + "REMOVE": "Eliminați", + "VERIFY": "Verificați", + "FINISH": "Finalizați", + "FINISHED": "Închideți", + "CHANGE": "Modificați", + "REACTIVATE": "Reactivați", + "ACTIVATE": "Activați", + "DEACTIVATE": "Dezactivați", + "REFRESH": "Reîmprospătați", + "LOGIN": "Conectați-vă", + "EDIT": "Editați", + "PIN": "Fixați / Anulați fixarea", + "CONFIGURE": "Configurați", + "SEND": "Trimiteți", + "NEWVALUE": "Valoare nouă", + "RESTORE": "Restabiliți", + "CONTINUEWITHOUTSAVE": "Continuați fără a salva", + "OF": "din", + "PREVIOUS": "Anterior", + "NEXT": "Următorul", + "MORE": "mai multe", + "STEP": "Pasul", + "SETUP": "Configurați", + "TEST": "Testați", + "UNSAVEDCHANGES": "Modificări nesalvate", + "UNSAVED": { + "DIALOG": { + "DESCRIPTION": "Sigur doriți să renunțați la această acțiune nouă? Acțiunea dvs. se va pierde", + "CANCEL": "Anulați", + "DISCARD": "Renunțați" + } + }, + "TABLE": { + "SHOWUSER": "Afișați utilizatorul {{value}}" + }, + "DOWNLOAD": "Descărcați", + "APPLY": "Aplicați" + }, + "ACTIONSTWO": { + "BETA_NOTE": "În prezent utilizați noua versiune Actions V2, care este în faza beta. Versiunea anterioară 1 este încă disponibilă, dar va fi întreruptă în viitor. Vă rugăm să raportați orice problemă sau feedback.", + "EXECUTION": { + "TITLE": "Acțiuni", + "DESCRIPTION": "Acțiunile vă permit să rulați cod personalizat ca răspuns la cereri API, evenimente sau funcții specifice. Folosiți-le pentru a extinde Zitadel, a automatiza fluxurile de lucru și a vă integra cu alte sisteme.", + "TYPES": { + "request": "Cerere", + "response": "Răspuns", + "event": "Evenimente", + "function": "Funcție" + }, + "DIALOG": { + "CREATE_TITLE": "Creează o Acțiune", + "UPDATE_TITLE": "Actualizează o Acțiune", + "TYPE": { + "DESCRIPTION": "Selectați când doriți să rulați această Acțiune", + "REQUEST": { + "TITLE": "Cerere", + "DESCRIPTION": "Cereri care apar în Zitadel. Acesta ar putea fi ceva de genul unui apel de cerere de autentificare." + }, + "RESPONSE": { + "TITLE": "Răspuns", + "DESCRIPTION": "Un răspuns la o cerere în Zitadel. Gândiți-vă la răspunsul pe care îl primiți la preluarea unui utilizator." + }, + "EVENTS": { + "TITLE": "Evenimente", + "DESCRIPTION": "Evenimente care se întâmplă în Zitadel. Acesta ar putea fi orice, cum ar fi un utilizator care își creează un cont, o autentificare reușită etc." + }, + "FUNCTIONS": { + "TITLE": "Funcții", + "DESCRIPTION": "Funcții pe care le puteți apela în Zitadel. Acesta ar putea fi orice, de la trimiterea unui e-mail la crearea unui utilizator." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Alegeți dacă această acțiune se aplică tuturor cererilor, unui serviciu specific (de exemplu, gestionarea utilizatorilor) sau unei singure cereri (de exemplu, crearea unui utilizator).", + "ALL": { + "TITLE": "Toate", + "DESCRIPTION": "Selectați aceasta dacă doriți să rulați acțiunea la fiecare cerere" + }, + "ALL_EVENTS": "Selectează aceasta dacă vrei să rulezi acțiunea ta la fiecare eveniment", + "SELECT_SERVICE": { + "TITLE": "Selectați Serviciul", + "DESCRIPTION": "Alegeți un Serviciu Zitadel pentru acțiunea dvs." + }, + "SELECT_METHOD": { + "TITLE": "Selectați Metoda", + "DESCRIPTION": "Dacă doriți să rulați numai la o cerere specifică, selectați-o aici", + "NOTE": "Dacă nu selectați o metodă, acțiunea dvs. va rula la fiecare cerere din serviciul selectat." + }, + "FUNCTIONNAME": { + "TITLE": "Numele Funcției", + "DESCRIPTION": "Alegeți funcția pe care doriți să o rulați" + }, + "SELECT_GROUP": { + "TITLE": "Setează Grupul", + "DESCRIPTION": "Dacă doriți să rulați numai pe un grup de evenimente, setați grupul aici" + }, + "SELECT_EVENT": { + "TITLE": "Selectați Evenimentul", + "DESCRIPTION": "Dacă doriți să rulați numai la un eveniment specific, specificați-l aici" + } + }, + "TARGET": { + "DESCRIPTION": "Puteți alege să rulați o țintă sau să o rulați în aceleași condiții ca și alte ținte.", + "TARGET": { + "DESCRIPTION": "Ținta pe care doriți să o rulați pentru această acțiune" + }, + "CONDITIONS": { + "DESCRIPTION": "Condiții de Execuție" + } + } + }, + "TABLE": { + "CONDITION": "Condiție", + "TYPE": "Tip", + "TARGET": "Țintă", + "CREATIONDATE": "Data Creării" + } + }, + "TARGET": { + "TITLE": "Ținte", + "DESCRIPTION": "O țintă este destinația codului pe care doriți să-l rulați dintr-o acțiune. Creați o țintă aici și adăugați-o la acțiunile dvs.", + "CREATE": { + "TITLE": "Creează-ți Ținta", + "DESCRIPTION": "Creați-vă propria țintă în afara Zitadel", + "NAME": "Nume", + "NAME_DESCRIPTION": "Dați țintei dvs. un nume clar, descriptiv, pentru a o identifica ușor mai târziu", + "TYPE": "Tip", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Apel REST", + "restAsync": "REST Asincron" + }, + "TYPES_DESCRIPTION": "Webhook, apelul gestionează codul de stare, dar răspunsul este irelevant\nCall, apelul gestionează codul de stare și răspunsul\nAsync, apelul nu gestionează nici codul de stare, nici răspunsul, dar poate fi apelat în paralel cu alte Ținte", + "ENDPOINT": "Punct Final", + "ENDPOINT_DESCRIPTION": "Introduceți punctul final unde este găzduit codul dvs. Asigurați-vă că este accesibil pentru noi!", + "TIMEOUT": "Timeout", + "TIMEOUT_DESCRIPTION": "Setați timpul maxim pe care ținta dvs. îl are pentru a răspunde. Dacă durează mai mult, vom opri cererea.", + "INTERRUPT_ON_ERROR": "Întrerupe la Eroare", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Opriți toate execuțiile când țintele returnează o eroare", + "INTERRUPT_ON_ERROR_WARNING": "Atenție: „Întrerupe la eroare” oprește operațiunile în caz de eșec, riscând blocarea accesului. Testați cu această opțiune dezactivată pentru a evita blocarea autentificării/creării.", + "AWAIT_RESPONSE": "Așteaptă Răspuns", + "AWAIT_RESPONSE_DESCRIPTION": "Vom aștepta un răspuns înainte de a face altceva. Util dacă intenționați să utilizați mai multe ținte pentru o singură acțiune" + }, + "TABLE": { + "NAME": "Nume", + "ENDPOINT": "Punct Final", + "CREATIONDATE": "Data Creării", + "REORDER": "Reordonați" + } + } + }, + "MEMBERROLES": { + "IAM_OWNER": "Are control asupra întregii instanțe, inclusiv asupra tuturor organizațiilor", + "IAM_OWNER_VIEWER": "Are permisiunea de a revizui întreaga instanță, inclusiv toate organizațiile", + "IAM_ORG_MANAGER": "Are permisiunea de a crea și gestiona organizații", + "IAM_USER_MANAGER": "Are permisiunea de a crea și gestiona utilizatori", + "IAM_ADMIN_IMPERSONATOR": "Are permisiunea de a impersona administratorul și utilizatorii finali din toate organizațiile", + "IAM_END_USER_IMPERSONATOR": "Are permisiunea de a impersona utilizatorii finali din toate organizațiile", + "IAM_LOGIN_CLIENT": "Are permisiunea de a gestiona clientii de login", + "ORG_OWNER": "Are permisiunea asupra întregii organizații", + "ORG_USER_MANAGER": "Are permisiunea de a crea și gestiona utilizatorii organizației", + "ORG_OWNER_VIEWER": "Are permisiunea de a revizui întreaga organizație", + "ORG_USER_PERMISSION_EDITOR": "Are permisiunea de a gestiona granturile utilizatorilor", + "ORG_PROJECT_PERMISSION_EDITOR": "Are permisiunea de a gestiona granturile proiectelor", + "ORG_PROJECT_CREATOR": "Are permisiunea de a-și crea propriile proiecte și setările de bază", + "ORG_ADMIN_IMPERSONATOR": "Are permisiunea de a impersona administratorul și utilizatorii finali din organizație", + "ORG_END_USER_IMPERSONATOR": "Are permisiunea de a impersona utilizatorii finali din organizație", + "PROJECT_OWNER": "Are permisiunea asupra întregului proiect", + "PROJECT_OWNER_VIEWER": "Are permisiunea de a revizui întregul proiect", + "PROJECT_OWNER_GLOBAL": "Are permisiunea asupra întregului proiect", + "PROJECT_OWNER_VIEWER_GLOBAL": "Are permisiunea de a revizui întregul proiect", + "PROJECT_GRANT_OWNER": "Are permisiunea de a gestiona grantul proiectului", + "PROJECT_GRANT_OWNER_VIEWER": "Are permisiunea de a revizui grantul proiectului" + }, + "OVERLAYS": { + "ORGSWITCHER": { + "TEXT": "Toate setările organizației și tabelele din consolă se bazează pe o organizație selectată. Faceți clic pe acest buton pentru a schimba organizația sau pentru a crea una nouă." + }, + "INSTANCE": { + "TEXT": "Faceți clic aici pentru a ajunge la setările implicite. Rețineți că aveți acces la acest buton numai dacă aveți permisiuni îmbunătățite." + }, + "PROFILE": { + "TEXT": "Aici puteți comuta între conturile dvs. de utilizator și vă puteți gestiona sesiunile și profilul." + }, + "NAV": { + "TEXT": "Această navigare se modifică în funcție de organizația selectată de mai sus sau de instanța dvs." + }, + "CONTEXTCHANGED": { + "TEXT": "Contextul organizației s-a modificat." + }, + "SWITCHEDTOINSTANCE": { + "TEXT": "Vizualizarea tocmai s-a schimbat în instanță!" + } + }, + "FILTER": { + "TITLE": "Filtru", + "STATE": "Stare", + "DISPLAYNAME": "Numele afișat al utilizatorului", + "EMAIL": "E-mail", + "USERNAME": "Numele utilizatorului", + "ORGNAME": "Numele organizației", + "PRIMARYDOMAIN": "Domeniu principal", + "PROJECTNAME": "Numele proiectului", + "RESOURCEOWNER": "Proprietarul resursei", + "METHODS": { + "5": "conține", + "7": "se termină cu", + "1": "este egal cu" + } + }, + "KEYBOARDSHORTCUTS": { + "TITLE": "Comenzi rapide de la tastatură", + "UNDERORGCONTEXT": "În cadrul paginilor organizației", + "SIDEWIDE": "Comenzi rapide la nivelul site-ului", + "SHORTCUTS": { + "HOME": "Du-te la Acasă", + "INSTANCE": "Du-te la Instanță", + "ORG": "Du-te la Organizație", + "ORGSETTINGS": "Du-te la Setările organizației", + "ORGSWITCHER": "Schimbă organizația", + "ME": "Du-te la propriul profil", + "PROJECTS": "Du-te la Proiecte", + "USERS": "Du-te la Utilizatori", + "USERGRANTS": "Du-te la Autorizații", + "ACTIONS": "Du-te la Acțiuni și Fluxuri", + "DOMAINS": "Du-te la Domenii" + } + }, + "RESOURCEID": "ID-ul resursei", + "NAME": "Nume", + "VERSION": "Versiune", + "TABLE": { + "NOROWS": "Fără date" + }, + "ERRORS": { + "REQUIRED": "Vă rugăm să completați acest câmp.", + "ATLEASTONE": "Furnizați cel puțin o valoare.", + "TOKENINVALID": { + "TITLE": "Tokenul dvs. de autorizare a expirat.", + "DESCRIPTION": "Faceți clic pe butonul de mai jos pentru a vă conecta din nou." + }, + "EXHAUSTED": { + "TITLE": "Instanța dvs. este blocată.", + "DESCRIPTION": "Solicitați administratorului instanței ZITADEL să actualizeze abonamentul." + }, + "INVALID_FORMAT": "Formatarea este nevalidă.", + "NOTANEMAIL": "Valoarea furnizată nu este o adresă de e-mail.", + "MINLENGTH": "Trebuie să aibă cel puțin {{requiredLength}} caractere.", + "MAXLENGTH": "Trebuie să aibă mai puțin de {{requiredLength}} caractere.", + "UPPERCASEMISSING": "Trebuie să includă o literă majusculă.", + "LOWERCASEMISSING": "Trebuie să includă o literă minusculă.", + "SYMBOLERROR": "Trebuie să includă un simbol sau un semn de punctuație.", + "NUMBERERROR": "Trebuie să includă o cifră.", + "PWNOTEQUAL": "Parolele furnizate nu se potrivesc.", + "PHONE": "Numărul de telefon trebuie să înceapă cu +." + }, + "USER": { + "SETTINGS": { + "TITLE": "Setări", + "GENERAL": "General", + "IDP": "Furnizori de identitate", + "SECURITY": "Parolă și securitate", + "KEYS": "Chei", + "PAT": "Tokenuri de acces personal", + "USERGRANTS": "Autorizații", + "MEMBERSHIPS": "Afilieri", + "METADATA": "Metadata" + }, + "TITLE": "Informații personale", + "DESCRIPTION": "Gestionați-vă informațiile și setările de securitate.", + "PAGES": { + "TITLE": "Utilizator", + "DETAIL": "Detalii", + "CREATE": "Creare", + "MY": "Informațiile mele", + "LOGINNAMES": "Nume de conectare", + "LOGINMETHODS": "Metode de conectare", + "LOGINNAMESDESC": "Acestea sunt numele dvs. de conectare:", + "NOUSER": "Niciun utilizator asociat.", + "REACTIVATE": "Reactivați", + "DEACTIVATE": "Dezactivați", + "FILTER": "Filtru", + "STATE": "Stare", + "DELETE": "Ștergeți utilizatorul", + "UNLOCK": "Deblocați utilizatorul", + "GENERATESECRET": "Generați secretul clientului", + "REMOVESECRET": "Eliminați secretul clientului", + "LOCKEDDESCRIPTION": "Acest utilizator a fost blocat din cauza depășirii numărului maxim de încercări de conectare și trebuie deblocat pentru a fi utilizat din nou.", + "DELETEACCOUNT": "Ștergeți contul", + "DELETEACCOUNT_DESC": "Dacă efectuați această acțiune, veți fi deconectat și nu veți mai avea acces la contul dvs. Această acțiune nu este reversibilă, așa că vă rugăm să continuați cu prudență.", + "DELETEACCOUNT_BTN": "Ștergeți contul", + "DELETEACCOUNT_SUCCESS": "Cont șters cu succes!" + }, + "DETAILS": { + "DATECREATED": "Creat", + "DATECHANGED": "Modificat" + }, + "DIALOG": { + "DELETE_TITLE": "Ștergeți utilizatorul", + "DELETE_SELF_TITLE": "Ștergeți contul", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți definitiv un utilizator. Sigur doriți să continuați?", + "DELETE_SELF_DESCRIPTION": "Sunteți pe cale să ștergeți definitiv contul dvs. personal. Aceasta vă va deconecta și vă va șterge utilizatorul. Această acțiune nu poate fi anulată!", + "DELETE_AUTH_DESCRIPTION": "Sunteți pe cale să ștergeți definitiv contul dvs. personal. Sigur doriți să continuați?", + "TYPEUSERNAME": "Tastați '{{value}}' pentru a confirma și a șterge utilizatorul.", + "USERNAME": "Nume de conectare", + "DELETE_BTN": "Ștergeți definitiv" + }, + "SENDEMAILDIALOG": { + "TITLE": "Trimiteți notificarea prin e-mail", + "DESCRIPTION": "Faceți clic pe butonul de mai jos pentru a trimite o notificare la adresa de e-mail curentă sau modificați adresa de e-mail din câmp.", + "NEWEMAIL": "Adresă de e-mail nouă" + }, + "SECRETDIALOG": { + "CLIENTSECRET": "Secretul clientului", + "CLIENTSECRET_DESCRIPTION": "Păstrați secretul clientului într-un loc sigur, deoarece va dispărea odată ce dialogul este închis." + }, + "TABLE": { + "DEACTIVATE": "Dezactivați", + "ACTIVATE": "Activați", + "CHANGEDATE": "Ultima modificare", + "CREATIONDATE": "Creat la", + "FILTER": { + "0": "Filtrați după Numele afișat", + "1": "Filtrați după Numele de utilizator", + "2": "filtrați după Numele afișat", + "3": "filtrați după Numele de utilizator", + "4": "filtrați după E-mail", + "5": "filtrați după Numele afișat", + "10": "filtrați după numele organizației", + "12": "filtrați după numele proiectului" + }, + "EMPTY": "Nicio înregistrare" + }, + "PASSWORDLESS": { + "SEND": "Trimiteți linkul de înregistrare", + "TABLETYPE": "Tip", + "TABLESTATE": "Stare", + "NAME": "Nume", + "EMPTY": "Niciun dispozitiv setat", + "TITLE": "Autentificare fără parolă", + "DESCRIPTION": "Adăugați metode de autentificare bazate pe WebAuthn pentru a vă conecta la ZITADEL fără parolă.", + "MANAGE_DESCRIPTION": "Gestionați metodele de al doilea factor ale utilizatorilor dvs.", + "U2F": "Adăugați metodă", + "U2F_DIALOG_TITLE": "Verificați autentificatorul", + "U2F_DIALOG_DESCRIPTION": "Introduceți un nume pentru conectarea fără parolă utilizată", + "U2F_SUCCESS": "Autentificare fără parolă creată cu succes!", + "U2F_ERROR": "A apărut o eroare în timpul configurării!", + "U2F_NAME": "Numele autentificatorului", + "TYPE": { + "0": "Niciun MFA definit", + "1": "Parolă unică (OTP)", + "2": "Amprentă, chei de securitate, Face ID și altele" + }, + "STATE": { + "0": "Nicio stare", + "1": "Nu este gata", + "2": "Gata", + "3": "Șters" + }, + "DIALOG": { + "DELETE_TITLE": "Eliminați metoda de autentificare fără parolă", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți o metodă de autentificare fără parolă. Sigur doriți să continuați?", + "ADD_TITLE": "Autentificare fără parolă", + "ADD_DESCRIPTION": "Selectați una dintre opțiunile disponibile pentru a crea o metodă de autentificare fără parolă.", + "SEND_DESCRIPTION": "Trimiteți-vă un link de înregistrare la adresa dvs. de e-mail.", + "SEND": "Trimiteți linkul de înregistrare", + "SENT": "E-mailul a fost livrat cu succes. Verificați-vă căsuța poștală pentru a continua cu configurarea.", + "QRCODE_DESCRIPTION": "Generați codul QR pentru scanare cu un alt dispozitiv.", + "QRCODE": "Generați codul QR", + "QRCODE_SCAN": "Scanați acest cod QR pentru a continua cu configurarea pe dispozitivul dvs.", + "NEW_DESCRIPTION": "Utilizați acest dispozitiv pentru a configura Autentificarea fără parolă.", + "NEW": "Adăugați nou" + } + }, + "MFA": { + "TABLETYPE": "Tip", + "TABLESTATE": "Stare", + "NAME": "Nume", + "EMPTY": "Niciun factor suplimentar", + "TITLE": "Autentificare multifactor", + "DESCRIPTION": "Adăugați un al doilea factor pentru a asigura o securitate optimă pentru contul dvs.", + "MANAGE_DESCRIPTION": "Gestionați metodele de al doilea factor ale utilizatorilor dvs.", + "ADD": "Adăugați factor", + "OTP": "Aplicație de autentificare pentru TOTP (Parolă unică bazată pe timp)", + "OTP_DIALOG_TITLE": "Adăugați OTP", + "OTP_DIALOG_DESCRIPTION": "Scanați codul QR cu o aplicație de autentificare și introduceți codul de mai jos pentru a verifica și a activa metoda OTP.", + "U2F": "Amprentă, chei de securitate, Face ID și altele", + "U2F_DIALOG_TITLE": "Verificați factorul", + "U2F_DIALOG_DESCRIPTION": "Introduceți un nume pentru multifactorul universal utilizat.", + "U2F_SUCCESS": "Factor adăugat cu succes!", + "U2F_ERROR": "A apărut o eroare în timpul configurării!", + "U2F_NAME": "Numele autentificatorului", + "OTPSMS": "OTP (Parolă unică) cu SMS", + "OTPEMAIL": "OTP (Parolă unică) cu e-mail", + "SETUPOTPSMSDESCRIPTION": "Doriți să configurați acest număr de telefon ca al doilea factor OTP (parolă unică)?", + "OTPSMSSUCCESS": "Factorul OTP configurat cu succes.", + "OTPSMSPHONEMUSTBEVERIFIED": "Telefonul dvs. trebuie să fie verificat pentru a utiliza această metodă.", + "OTPEMAILSUCCESS": "Factorul OTP configurat cu succes.", + "TYPE": { + "0": "Niciun MFA definit", + "1": "Parolă unică (OTP)", + "2": "Amprentă, chei de securitate, Face ID și altele" + }, + "STATE": { + "0": "Nicio stare", + "1": "Nu este gata", + "2": "Gata", + "3": "Șters" + }, + "DIALOG": { + "MFA_DELETE_TITLE": "Eliminați al doilea factor", + "MFA_DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți un al doilea factor. Sigur doriți să continuați?", + "ADD_MFA_TITLE": "Adăugați al doilea factor", + "ADD_MFA_DESCRIPTION": "Selectați una dintre următoarele opțiuni." + } + }, + "EXTERNALIDP": { + "TITLE": "Furnizori de identitate externi", + "DESC": "", + "IDPCONFIGID": "ID-ul config-ului IDP", + "IDPNAME": "Numele IDP", + "USERDISPLAYNAME": "Nume extern", + "EXTERNALUSERID": "ID-ul utilizatorului extern", + "EMPTY": "Niciun IdP extern găsit", + "DIALOG": { + "DELETE_TITLE": "Eliminați IdP", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți un furnizor de identitate de la un utilizator. Doriți cu adevărat să continuați?" + } + }, + "CREATE": { + "TITLE": "Creați un utilizator nou", + "DESCRIPTION": "Vă rugăm să furnizați informațiile necesare.", + "NAMEANDEMAILSECTION": "Nume și e-mail", + "GENDERLANGSECTION": "Sex și limbă", + "PHONESECTION": "Numere de telefon", + "PASSWORDSECTION": "Parola inițială", + "ADDRESSANDPHONESECTION": "Număr de telefon", + "INITMAILDESCRIPTION": "Dacă ambele opțiuni sunt selectate, nu va fi trimis niciun e-mail pentru inițializare. Dacă este selectată doar una dintre opțiuni, va fi trimis un e-mail pentru a furniza / verifica datele.", + "SETUPAUTHENTICATIONLATER": "Configurați autentificarea mai târziu pentru acest utilizator.", + "INVITATION": "Trimiteți un e-mail de invitație pentru configurarea autentificării și verificarea e-mailului.", + "INITIALPASSWORD": "Setați o parolă inițială pentru utilizator." + }, + "CODEDIALOG": { + "TITLE": "Verificați numărul de telefon", + "DESCRIPTION": "Introduceți codul pe care l-ați primit prin mesaj text pentru a vă verifica numărul de telefon.", + "CODE": "Cod" + }, + "DATA": { + "STATE": "Stare", + "STATE0": "Necunoscut", + "STATE1": "Activ", + "STATE2": "Inactiv", + "STATE3": "Șters", + "STATE4": "Blocat", + "STATE5": "Suspendat", + "STATE6": "Inițial" + }, + "PROFILE": { + "TITLE": "Profil", + "EMAIL": "E-mail", + "PHONE": "Număr de telefon", + "PHONE_HINT": "Utilizați simbolul + urmat de codul de țară, sau selectați țara din lista derulantă și introduceți în cele din urmă numărul de telefon", + "PHONE_VERIFIED": "Numărul de telefon verificat", + "SEND_SMS": "Trimiteți SMS de verificare", + "SEND_EMAIL": "Trimiteți E-mail", + "USERNAME": "Nume de utilizator", + "CHANGEUSERNAME": "modificați", + "CHANGEUSERNAME_TITLE": "Modificați numele de utilizator", + "CHANGEUSERNAME_DESC": "Introduceți noul nume în câmpul de mai jos.", + "FIRSTNAME": "Prenume", + "LASTNAME": "Nume de familie", + "NICKNAME": "Poreclă", + "DISPLAYNAME": "Numele afișat", + "PREFERREDLOGINNAME": "Numele de conectare preferat", + "PREFERRED_LANGUAGE": "Limbă", + "GENDER": "Sex", + "PASSWORD": "Parolă", + "AVATAR": { + "UPLOADTITLE": "Încărcați-vă poza de profil", + "UPLOADBTN": "Alegeți fișier", + "UPLOAD": "Încărcați", + "CURRENT": "Poza actuală", + "PREVIEW": "Previzualizare", + "DELETESUCCESS": "Șters cu succes!", + "CROPPERERROR": "A apărut o eroare în timpul încărcării fișierului dvs. Încercați un format și o dimensiune diferite, dacă este necesar." + }, + "COUNTRY": "Țară" + }, + "MACHINE": { + "TITLE": "Detalii utilizator de serviciu", + "USERNAME": "Nume de utilizator", + "NAME": "Nume", + "DESCRIPTION": "Descriere", + "KEYSTITLE": "Chei", + "KEYSDESC": "Definiți-vă cheile și adăugați o dată de expirare opțională.", + "TOKENSTITLE": "Tokenuri de acces personal", + "TOKENSDESC": "Tokenurile de acces personal funcționează ca tokenurile de acces OAuth obișnuite.", + "ID": "ID cheie", + "TYPE": "Tip", + "EXPIRATIONDATE": "Data de expirare", + "CHOOSEDATEAFTER": "Introduceți o expirare validă după", + "CHOOSEEXPIRY": "Selectați o dată de expirare", + "CREATIONDATE": "Data creării", + "KEYDETAILS": "Detalii cheie", + "ACCESSTOKENTYPE": "Tipul tokenului de acces", + "ACCESSTOKENTYPES": { + "0": "Bearer", + "1": "JWT" + }, + "ADD": { + "TITLE": "Adăugați cheie", + "DESCRIPTION": "Selectați tipul cheii și alegeți o dată de expirare opțională." + }, + "ADDED": { + "TITLE": "Cheia a fost creată", + "DESCRIPTION": "Descărcați cheia, deoarece nu va mai fi vizibilă după închiderea acestui dialog!" + }, + "KEYTYPES": { + "1": "JSON" + }, + "DIALOG": { + "DELETE_KEY": { + "TITLE": "Ștergeți cheia", + "DESCRIPTION": "Doriți să ștergeți cheia selectată? Această acțiune nu poate fi anulată." + } + } + }, + "PASSWORD": { + "TITLE": "Parolă", + "LABEL": "O parolă sigură ajută la protejarea contului", + "DESCRIPTION": "Introduceți noua parolă conform politicii de mai jos.", + "OLD": "Parola curentă", + "NEW": "Parolă nouă", + "CONFIRM": "Confirmați parola nouă", + "NEWINITIAL": "Parolă", + "CONFIRMINITIAL": "Confirmați parola", + "RESET": "Resetați parola curentă", + "SET": "Setați parola nouă", + "RESENDNOTIFICATION": "Trimiteți linkul de resetare a parolei", + "REQUIRED": "Lipsesc unele câmpuri obligatorii.", + "MINLENGTHERROR": "Trebuie să aibă cel puțin {{value}} caractere.", + "MAXLENGTHERROR": "Trebuie să aibă mai puțin de {{value}} caractere." + }, + "ID": "ID", + "EMAIL": "E-mail", + "PHONE": "Număr de telefon", + "PHONEEMPTY": "Niciun număr de telefon definit", + "PHONEVERIFIED": "Numărul de telefon verificat.", + "EMAILVERIFIED": "E-mail verificat", + "NOTVERIFIED": "neverificat", + "PREFERRED_LOGINNAME": "Nume de conectare preferat", + "ISINITIAL": "Utilizatorul nu este încă activ.", + "LOGINMETHODS": { + "TITLE": "Informații de contact", + "DESCRIPTION": "Informațiile furnizate sunt utilizate pentru a vă trimite informații importante, cum ar fi e-mailurile de resetare a parolei.", + "EMAIL": { + "TITLE": "E-mail", + "VALID": "validat", + "ISVERIFIED": "E-mail verificat", + "ISVERIFIEDDESC": "Dacă e-mailul este indicat ca verificat, nu va fi făcută nicio solicitare de verificare a e-mailului.", + "RESEND": "Retrimiteți e-mailul de verificare", + "EDITTITLE": "Modificați e-mailul", + "EDITDESC": "Introduceți noul e-mail în câmpul de mai jos." + }, + "PHONE": { + "TITLE": "Telefon", + "VALID": "validat", + "RESEND": "Retrimiteți mesajul text de verificare", + "EDITTITLE": "Modificați numărul", + "EDITVALUE": "Număr de telefon", + "EDITDESC": "Introduceți noul număr de telefon în câmpul de mai jos.", + "DELETETITLE": "Ștergeți numărul de telefon", + "DELETEDESC": "Doriți cu adevărat să ștergeți numărul de telefon", + "OTPSMSREMOVALWARNING": "Acest cont utilizează acest număr de telefon ca al doilea factor. Nu veți putea să îl utilizați după ce continuați." + }, + "RESENDCODE": "Retrimiteți codul", + "ENTERCODE": "Verificați", + "ENTERCODE_DESC": "Verificați codul" + }, + "GRANTS": { + "TITLE": "Granturi de utilizator", + "DESCRIPTION": "Acordați acestui utilizator acces la anumite proiecte", + "CREATE": { + "TITLE": "Creați grant de utilizator", + "DESCRIPTION": "Căutați organizația, proiectul și rolurile de proiect corespunzătoare." + }, + "PROJECTNAME": "Numele proiectului", + "PROJECT-OWNED": "Proiect", + "PROJECT-GRANTED": "Proiect acordat", + "FILTER": { + "0": "filtrați după utilizator", + "1": "filtrați după domeniu", + "2": "filtrați după numele proiectului", + "3": "filtrați după numele rolului" + } + }, + "STATE": { + "0": "Necunoscut", + "1": "Activ", + "2": "Inactiv", + "3": "Șters", + "4": "Blocat", + "5": "Suspendat", + "6": "Inițial" + }, + "STATEV2": { + "0": "Necunoscut", + "1": "Activ", + "2": "Inactiv", + "3": "Șters", + "4": "Blocat", + "5": "Inițial" + }, + "SEARCH": { + "ADDITIONAL": "Nume de conectare (organizația curentă)", + "ADDITIONAL-EXTERNAL": "Nume de conectare (organizație externă)" + }, + "TARGET": { + "SELF": "Dacă doriți să acordați un utilizator al unei alte organizații", + "EXTERNAL": "Pentru a acorda un utilizator al organizației dvs.", + "CLICKHERE": "faceți clic aici" + }, + "SIGNEDOUT": "Sunteți deconectat. Faceți clic pe butonul \"Conectare\" pentru a vă conecta din nou.", + "SIGNEDOUT_BTN": "Conectare", + "EDITACCOUNT": "Editați contul", + "ADDACCOUNT": "Conectați-vă cu un alt cont", + "RESENDINITIALEMAIL": "Retrimiteți e-mailul de activare", + "RESENDEMAILNOTIFICATION": "Retrimiteți notificarea prin e-mail", + "TOAST": { + "CREATED": "Utilizator creat cu succes.", + "SAVED": "Profil salvat cu succes.", + "USERNAMECHANGED": "Nume de utilizator modificat.", + "EMAILSAVED": "E-mail salvat cu succes.", + "INITEMAILSENT": "E-mail de inițializare trimis.", + "PHONESAVED": "Telefon salvat cu succes.", + "PHONEREMOVED": "Telefon eliminat.", + "PHONEVERIFIED": "Telefon verificat cu succes.", + "PHONEVERIFICATIONSENT": "Codul de verificare a telefonului trimis.", + "EMAILVERIFICATIONSENT": "Codul de verificare a e-mailului trimis.", + "OTPREMOVED": "OTP eliminat.", + "U2FREMOVED": "Factor eliminat.", + "PASSWORDLESSREMOVED": "Fără parolă eliminat.", + "INITIALPASSWORDSET": "Parola inițială setată.", + "PASSWORDNOTIFICATIONSENT": "Notificarea de modificare a parolei trimisă.", + "PASSWORDCHANGED": "Parola modificată cu succes.", + "REACTIVATED": "Utilizatorul reactivat.", + "DEACTIVATED": "Utilizatorul dezactivat.", + "SELECTEDREACTIVATED": "Utilizatorii selectați au fost reactivați.", + "SELECTEDDEACTIVATED": "Utilizatorii selectați au fost dezactivați.", + "SELECTEDKEYSDELETED": "Cheile selectate au fost șterse.", + "KEYADDED": "Cheie adăugată!", + "MACHINEADDED": "Utilizator de serviciu creat!", + "DELETED": "Utilizator șters cu succes!", + "UNLOCKED": "Utilizator deblocat cu succes!", + "PASSWORDLESSREGISTRATIONSENT": "Link de înregistrare trimis cu succes.", + "SECRETGENERATED": "Secret generat cu succes!", + "SECRETREMOVED": "Secret eliminat cu succes!" + }, + "MEMBERSHIPS": { + "TITLE": "Roluri de manager ZITADEL", + "DESCRIPTION": "Acestea sunt toate granturile de membru ale utilizatorului. Puteți să le modificați și în paginile de detalii ale organizației, proiectului sau IAM.", + "ORGCONTEXT": "Vedeți toate organizațiile și proiectele care sunt legate de organizația selectată în prezent.", + "USERCONTEXT": "Vedeți toate organizațiile și proiectele la care sunteți autorizat. Inclusiv alte organizații.", + "CREATIONDATE": "Data creării", + "CHANGEDATE": "Ultima modificare", + "DISPLAYNAME": "Numele afișat", + "REMOVE": "Eliminați", + "TYPE": "Tip", + "ORGID": "ID-ul organizației", + "UPDATED": "Afilierea a fost actualizată.", + "NOPERMISSIONTOEDIT": "Vă lipsesc permisiunile necesare pentru a edita rolurile!", + "TYPES": { + "UNKNOWN": "Necunoscut", + "ORG": "Organizație", + "PROJECT": "Proiect", + "GRANTEDPROJECT": "Proiect acordat" + } + }, + "PERSONALACCESSTOKEN": { + "ID": "ID", + "TOKEN": "Token", + "ADD": { + "TITLE": "Generați un token de acces personal nou", + "DESCRIPTION": "Definiți o expirare personalizată pentru token.", + "CHOOSEEXPIRY": "Selectați o dată de expirare", + "CHOOSEDATEAFTER": "Introduceți o expirare validă după" + }, + "ADDED": { + "TITLE": "Token de acces personal", + "DESCRIPTION": "Asigurați-vă că vă copiați tokenul de acces personal. Nu veți mai putea să îl vedeți din nou!" + }, + "DELETE": { + "TITLE": "Ștergeți tokenul", + "DESCRIPTION": "Sunteți pe cale să ștergeți tokenul de acces personal. Sigur doriți să continuați?" + }, + "DELETED": "Token șters cu succes." + } + }, + "METADATA": { + "TITLE": "Metadata", + "KEY": "Cheie", + "VALUE": "Valoare", + "ADD": "Înregistrare nouă", + "SAVE": "Salvați", + "EMPTY": "Nicio metadată", + "SETSUCCESS": "Element salvat cu succes", + "REMOVESUCCESS": "Element șters cu succes" + }, + "FLOWS": { + "ID": "ID", + "NAME": "Nume", + "STATE": "Stare", + "STATES": { + "0": "nicio stare", + "1": "inactiv", + "2": "activ" + }, + "ADDTRIGGER": "Adăugați declanșator", + "FLOWCHANGED": "Fluxul a fost modificat cu succes", + "FLOWCLEARED": "Fluxul a fost resetat cu succes", + "TIMEOUT": "Timeout", + "TIMEOUTINSEC": "Timeout în secunde", + "ALLOWEDTOFAIL": "Permis să eșueze", + "ALLOWEDTOFAILWARN": { + "TITLE": "Avertisment", + "DESCRIPTION": "Dacă dezactivați această setare, este posibil ca utilizatorii din organizația dvs. să nu se poată conecta. În plus, nu veți mai putea accesa consola pentru a dezactiva acțiunea. Vă recomandăm să creați un utilizator administrator într-o organizație separată sau să testați mai întâi scripturile într-un mediu de dezvoltare sau într-o organizație de dezvoltare." + }, + "SCRIPT": "Script", + "FLOWTYPE": "Tip flux", + "TRIGGERTYPE": "Tip declanșator", + "ACTIONS": "Acțiuni", + "ACTIONSMAX": "Pe baza nivelului dvs., aveți la dispoziție un număr limitat de acțiuni ({{value}}). Asigurați-vă că dezactivați pe cele de care nu aveți nevoie sau luați în considerare actualizarea nivelului.", + "DIALOG": { + "ADD": { + "TITLE": "Creați o acțiune" + }, + "UPDATE": { + "TITLE": "Actualizați acțiunea" + }, + "DELETEACTION": { + "TITLE": "Ștergeți acțiunea?", + "DESCRIPTION": "Sunteți pe cale să ștergeți o acțiune. Aceasta nu poate fi inversată. Sigur doriți să continuați?", + "DELETE_SUCCESS": "Acțiunea a fost ștearsă cu succes." + }, + "CLEAR": { + "TITLE": "Ștergeți fluxul?", + "DESCRIPTION": "Sunteți pe cale să resetați fluxul împreună cu declanșatoarele și acțiunile sale. Această modificare nu poate fi restabilită. Sigur doriți să continuați?" + }, + "REMOVEACTIONSLIST": { + "TITLE": "Ștergeți acțiunile selectate?", + "DESCRIPTION": "Sigur doriți să ștergeți acțiunile selectate din flux?" + }, + "ABOUTNAME": "Numele acțiunii și numele funcției din javascript trebuie să fie aceleași" + }, + "TOAST": { + "ACTIONSSET": "Acțiuni setate", + "ACTIONREACTIVATED": "Acțiuni reactivate cu succes", + "ACTIONDEACTIVATED": "Acțiuni dezactivate cu succes" + } + }, + "IAM": { + "POLICIES": { + "TITLE": "Politici de sistem și setări de acces", + "DESCRIPTION": "Gestionați-vă politicile globale și setările de acces pentru gestionare." + }, + "EVENTSTORE": { + "TITLE": "Administrarea stocării IAM", + "DESCRIPTION": "Gestionați-vă vizualizările ZITADEL și evenimentele eșuate." + }, + "MEMBER": { + "TITLE": "Manageri", + "DESCRIPTION": "Acești manageri au permisiunea de a face modificări în instanța dvs." + }, + "PAGES": { + "STATE": "Stare", + "DOMAINLIST": "Domenii personalizate" + }, + "STATE": { + "0": "Nespecificat", + "1": "Creare", + "2": "Rulează", + "3": "Oprire", + "4": "Oprit" + }, + "VIEWS": { + "VIEWNAME": "Nume", + "DATABASE": "Bază de date", + "SEQUENCE": "Secvență", + "EVENTTIMESTAMP": "Marcaj de timp", + "LASTSPOOL": "Spool cu succes", + "ACTIONS": "Acțiuni", + "CLEAR": "Ștergeți", + "CLEARED": "Vizualizarea a fost ștearsă cu succes!", + "DIALOG": { + "VIEW_CLEAR_TITLE": "Ștergeți vizualizarea", + "VIEW_CLEAR_DESCRIPTION": "Sunteți pe cale să ștergeți o vizualizare. Ștergerea unei vizualizări creează un proces în timpul căruia este posibil ca datele să nu fie disponibile pentru utilizatorii finali. Sigur doriți să continuați?" + } + }, + "FAILEDEVENTS": { + "VIEWNAME": "Nume", + "DATABASE": "Bază de date", + "FAILEDSEQUENCE": "Secvență eșuată", + "FAILURECOUNT": "Număr de eșecuri", + "LASTFAILED": "Ultima eroare la", + "ERRORMESSAGE": "Mesaj de eroare", + "ACTIONS": "Acțiuni", + "DELETE": "Eliminați", + "DELETESUCCESS": "Evenimente eșuate eliminate." + }, + "EVENTS": { + "EDITOR": "Editor", + "EDITORID": "ID editor", + "AGGREGATE": "Agregat", + "AGGREGATEID": "ID agregat", + "AGGREGATETYPE": "Tip agregat", + "RESOURCEOWNER": "Proprietarul resursei", + "SEQUENCE": "Secvență", + "CREATIONDATE": "Creat la", + "TYPE": "Tip", + "PAYLOAD": "Sarcină utilă", + "FILTERS": { + "BTN": "Filtru", + "USER": { + "IDLABEL": "ID", + "CHECKBOX": "Filtrați după editor" + }, + "AGGREGATE": { + "TYPELABEL": "Tip agregat", + "IDLABEL": "ID", + "CHECKBOX": "Filtrați după agregat" + }, + "TYPE": { + "TYPELABEL": "Tip", + "CHECKBOX": "Filtrați după tip" + }, + "RESOURCEOWNER": { + "LABEL": "ID", + "CHECKBOX": "Filtrați după proprietarul resursei" + }, + "SEQUENCE": { + "LABEL": "Secvență", + "CHECKBOX": "Filtrați după secvență" + }, + "SORT": "Sortați", + "ASC": "Ascendent", + "DESC": "Descendent", + "CREATIONDATE": { + "RADIO_FROM": "De la", + "RADIO_RANGE": "Interval", + "LABEL_SINCE": "De când", + "LABEL_UNTIL": "Până la" + }, + "OTHER": "altele", + "OTHERS": "altele" + }, + "DIALOG": { + "TITLE": "Detalii eveniment" + } + }, + "TOAST": { + "MEMBERREMOVED": "Manager eliminat.", + "MEMBERSADDED": "Manageri adăugați.", + "MEMBERADDED": "Manager adăugat.", + "MEMBERCHANGED": "Manager modificat.", + "ROLEREMOVED": "Rol eliminat.", + "ROLECHANGED": "Rol modificat.", + "REACTIVATED": "Reactivat", + "DEACTIVATED": "Dezactivat" + } + }, + "ORG": { + "PAGES": { + "NAME": "Nume", + "ID": "ID", + "CREATIONDATE": "Data creării", + "DATECHANGED": "Modificat", + "FILTER": "Filtru", + "FILTERPLACEHOLDER": "Filtrați după nume", + "LIST": "Organizații", + "LISTDESCRIPTION": "Alegeți o organizație.", + "ACTIVE": "Activ", + "CREATE": "Creați organizație", + "DEACTIVATE": "Dezactivați organizația", + "REACTIVATE": "Reactivați organizația", + "NOPERMISSION": "Nu aveți permisiunea de a accesa setările organizației.", + "USERSELFACCOUNT": "Utilizați-vă contul personal ca proprietar al organizației", + "ORGDETAIL_TITLE": "Introduceți numele și domeniul noii dvs. organizații.", + "ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Introduceți numele noii dvs. organizații.", + "ORGDETAILUSER_TITLE": "Configurați proprietarul organizației", + "DELETE": "Ștergeți organizația", + "DEFAULTLABEL": "Implicit", + "SETASDEFAULT": "Setați ca organizație implicită", + "DEFAULTORGSET": "Organizația implicită a fost modificată cu succes", + "RENAME": { + "ACTION": "Redenumiți", + "TITLE": "Redenumiți organizația", + "DESCRIPTION": "Introduceți noul nume pentru organizația dvs.", + "BTN": "Redenumiți" + }, + "ORGDOMAIN": { + "TITLE": "Verificați proprietatea {{value}}", + "VERIFICATION": "Vă oferim două metode pentru a vă valida manual domeniul:", + "VERIFICATION_HTML": "- HTTP. Găzduiți un fișier temporar de verificare pe site-ul dvs. web", + "VERIFICATION_DNS": "- DNS. Creați o înregistrare DNS TXT", + "VERIFICATION_DNS_DESC": "Dacă gestionați {{ value }} și aveți acces la înregistrările DNS, puteți crea o înregistrare TXT nouă cu următoarele valori:", + "VERIFICATION_DNS_HOST_LABEL": "Gazdă:", + "VERIFICATION_DNS_CHALLENGE_LABEL": "Utilizați acest cod pentru valoarea înregistrării TXT:", + "VERIFICATION_HTTP_DESC": "Dacă aveți acces la găzduirea site-ului dvs. web, pur și simplu descărcați fișierul de verificare și încărcați-l la adresa URL furnizată", + "VERIFICATION_HTTP_URL_LABEL": "Adresă URL așteptată:", + "VERIFICATION_HTTP_FILE_LABEL": "Fișier de verificare:", + "VERIFICATION_SKIP": "Puteți sări peste verificarea deocamdată și să continuați să vă creați organizația, dar pentru a utiliza domeniul, acest pas trebuie finalizat!", + "VERIFICATION_VALIDATION_DESC": "Nu ștergeți codul de verificare, deoarece ZITADEL va verifica din când în când proprietatea domeniului dvs.", + "VERIFICATION_NEWTOKEN_TITLE": "Solicitați un token nou", + "VERIFICATION_VALIDATION_ONGOING": "Metoda {{ value }} a fost selectată pentru a vă verifica domeniul. Faceți clic pe buton pentru a declanșa o verificare de verificare sau pentru a reseta procesul de verificare.", + "VERIFICATION_SUCCESSFUL": "Domeniu verificat cu succes!", + "RESETMETHOD": "Resetați metoda de verificare" + }, + "DOWNLOAD_FILE": "Descărcați fișierul", + "SELECTORGTOOLTIP": "Selectați această organizație.", + "PRIMARYDOMAIN": "Domeniu principal", + "STATE": "Stare", + "USEPASSWORD": "Setați parola inițială", + "USEPASSWORDDESC": "Utilizatorul nu trebuie să seteze parola în timpul inițializării." + }, + "LIST": { + "TITLE": "Organizații", + "DESCRIPTION": "Acestea sunt organizațiile din instanța dvs." + }, + "DOMAINS": { + "NEW": "Adăugați domeniu", + "TITLE": "Domenii verificate", + "DESCRIPTION": "Configurați domeniile organizației dvs. Acest domeniu poate fi utilizat pentru descoperirea domeniului și sufixarea numelui de utilizator.", + "SETPRIMARY": "Setați ca principal", + "DELETE": { + "TITLE": "Ștergeți domeniul", + "DESCRIPTION": "Sunteți pe cale să ștergeți unul dintre domeniile dvs." + }, + "ADD": { + "TITLE": "Adăugați domeniu", + "DESCRIPTION": "Sunteți pe cale să adăugați un domeniu pentru organizația dvs. După un proces de succes, domeniul poate fi utilizat pentru descoperirea domeniului și ca sufix pentru utilizatorii dvs." + } + }, + "STATE": { + "0": "Nedefinit", + "1": "Activ", + "2": "Dezactivat" + }, + "MEMBER": { + "TITLE": "Manageri de organizație", + "DESCRIPTION": "Definiți utilizatorii care vă pot modifica preferințele organizațiilor." + }, + "TOAST": { + "UPDATED": "Organizația a fost actualizată cu succes.", + "DEACTIVATED": "Organizația a fost dezactivată.", + "REACTIVATED": "Organizația a fost reactivată.", + "DOMAINADDED": "Domeniu adăugat.", + "DOMAINREMOVED": "Domeniu eliminat.", + "MEMBERADDED": "Manager adăugat.", + "MEMBERREMOVED": "Manager eliminat.", + "MEMBERCHANGED": "Manager modificat.", + "SETPRIMARY": "Domeniu principal setat.", + "DELETED": "Organizația a fost ștearsă cu succes", + "DEFAULTORGNOTFOUND": "Organizația implicită nu a fost găsită", + "ORG_WAS_DELETED": "Organizația a fost ștearsă." + }, + "DIALOG": { + "DEACTIVATE": { + "TITLE": "Dezactivați organizația", + "DESCRIPTION": "Sunteți pe cale să vă dezactivați organizația. Utilizatorii nu se vor mai putea conecta după aceea. Sigur doriți să continuați?" + }, + "REACTIVATE": { + "TITLE": "Reactivați organizația", + "DESCRIPTION": "Sunteți pe cale să vă reactivați organizația. Utilizatorii se vor putea conecta din nou. Sigur doriți să continuați?" + }, + "DELETE": { + "TITLE": "Ștergeți organizația", + "DESCRIPTION": "Sunteți pe cale să vă ștergeți organizația. Aceasta inițiază un proces în care vor fi șterse toate datele legate de organizație. Nu puteți anula această acțiune deocamdată.", + "TYPENAME": "Tastați '{{value}}' pentru a vă șterge organizația.", + "ORGNAME": "Nume", + "BTN": "Ștergeți" + } + } + }, + "SETTINGS": { + "LIST": { + "ORGS": "Organizații", + "FEATURESETTINGS": "Caracteristici", + "LANGUAGES": "Limbi", + "LOGIN": "Comportament și securitate la conectare", + "LOCKOUT": "Blocare", + "AGE": "Expirarea parolei", + "COMPLEXITY": "Complexitatea parolei", + "NOTIFICATIONS": "Notificări", + "SMTP_PROVIDER": "Furnizor SMTP", + "SMS_PROVIDER": "Furnizor SMS/Telefon", + "NOTIFICATIONS_DESC": "Setări SMTP și SMS", + "MESSAGETEXTS": "Texte de mesaje", + "IDP": "Furnizori de identitate", + "VERIFIED_DOMAINS": "Domenii verificate", + "DOMAIN": "Setări domeniu", + "LOGINTEXTS": "Texte interfață de conectare", + "BRANDING": "Branding", + "PRIVACYPOLICY": "Linkuri externe", + "OIDC": "Durata de viață și expirarea tokenului OIDC", + "WEB_KEYS": "OIDC Web Keys", + "SECRETS": "Generator de secrete", + "SECURITY": "Setări de securitate", + "EVENTS": "Evenimente", + "FAILEDEVENTS": "Evenimente eșuate", + "VIEWS": "Vizualizări" + }, + "GROUPS": { + "GENERAL": "Informații generale", + "NOTIFICATIONS": "Notificări", + "LOGIN": "Conectare și acces", + "DOMAIN": "Domeniu", + "TEXTS": "Texte și limbi", + "APPEARANCE": "Aspect", + "OTHER": "Altele", + "STORAGE": "Stocare" + }, + "BETA": "BETA" + }, + "SETTING": { + "LANGUAGES": { + "DEFAULT": "Limbă implicită", + "ALLOWED": "Limbi permise", + "NOT_ALLOWED": "Limbi nepermise", + "ALLOW_ALL": "Permiteți toate", + "DISALLOW_ALL": "Nu permiteți toate", + "SETASDEFAULT": "Setați ca limbă implicită", + "DEFAULT_SAVED": "Limba implicită a fost salvată", + "ALLOWED_SAVED": "Limbile permise au fost salvate", + "OPTIONS": { + "de": "Deutsch", + "en": "English", + "es": "Español", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", + "pl": "Polski", + "zh": "简体中文", + "bg": "Български", + "pt": "Portuguese", + "mk": "Македонски", + "cs": "Čeština", + "ru": "Русский", + "nl": "Nederlands", + "sv": "Svenska", + "id": "Bahasa Indonesia", + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" + } + }, + "SMTP": { + "TITLE": "Furnizor SMTP", + "DESCRIPTION": "Descriere", + "SENDERADDRESS": "Adresă de e-mail expeditor", + "SENDERNAME": "Nume expeditor", + "REPLYTOADDRESS": "Adresă de răspuns", + "HOSTANDPORT": "Gazdă și port", + "USER": "Utilizator", + "PASSWORD": "Parolă", + "SETPASSWORD": "Setați parola SMTP", + "PASSWORDSET": "Parola SMTP a fost setată cu succes.", + "TLS": "Securitatea stratului de transport (TLS)", + "SAVED": "Salvat cu succes!", + "NOCHANGES": "Nicio modificare!", + "REQUIREDWARN": "Pentru a trimite notificări din domeniul dvs., trebuie să introduceți datele SMTP." + }, + "SMS": { + "PROVIDERS": "Furnizori", + "PROVIDER": "Furnizor SMS", + "ADDPROVIDER": "Adăugați furnizor SMS", + "ADDPROVIDERDESCRIPTION": "Alegeți unul dintre furnizorii disponibili și introduceți datele necesare.", + "REMOVEPROVIDER": "Eliminați furnizorul", + "REMOVEPROVIDER_DESC": "Sunteți pe cale să ștergeți o configurație de furnizor. Doriți să continuați?", + "SMSPROVIDERSTATE": { + "0": "Nespecificat", + "1": "Activ", + "2": "Inactiv" + }, + "ACTIVATED": "Furnizor activat.", + "DEACTIVATED": "Furnizor dezactivat.", + "TWILIO": { + "SID": "Sid", + "TOKEN": "Token", + "SENDERNUMBER": "Număr expeditor", + "VERIFYSERVICESID": "Verification Service Sid", + "VERIFYSERVICESID_DESCRIPTION": "Setarea unui Verification Service Sid, permite utilizarea Twilio Verify Service în loc de Messages Service pentru verificarea numerelor de telefon și a OTP SMS", + "ADDED": "Twilio adăugat cu succes.", + "UPDATED": "Twilio actualizat cu succes.", + "REMOVED": "Twilio eliminat", + "CHANGETOKEN": "Modificați tokenul", + "SETTOKEN": "Setați tokenul", + "TOKENSET": "Token setat cu succes." + } + }, + "SECRETS": { + "TYPES": "Tipuri de secrete", + "TYPE": { + "1": "E-mail de inițializare", + "2": "Verificarea e-mailului", + "3": "Verificarea telefonului", + "4": "Resetarea parolei", + "5": "Inițializarea fără parolă", + "6": "Secretul aplicației", + "7": "Parolă unică (OTP) - SMS", + "8": "Parolă unică (OTP) - E-mail" + }, + "EXPIRY": "Expirare (în minute)", + "INCLUDEDIGITS": "Includeți numere", + "INCLUDESYMBOLS": "Includeți simboluri", + "INCLUDELOWERLETTERS": "Includeți litere mici", + "INCLUDEUPPERLETTERS": "Includeți litere mari", + "LENGTH": "Lungime", + "UPDATED": "Setări actualizate." + }, + "SECURITY": { + "IFRAMETITLE": "iFrame", + "IFRAMEDESCRIPTION": "Această setare setează CSP pentru a permite încadrarea dintr-un set de domenii permise. Rețineți că prin activarea utilizării iFrames, vă asumați riscul de a permite clickjacking.", + "IFRAMEENABLED": "Permiteți iFrame", + "ALLOWEDORIGINS": "Adrese URL permise", + "IMPERSONATIONTITLE": "Impersonare", + "IMPERSONATIONENABLED": "Permiteți impersonarea", + "IMPERSONATIONDESCRIPTION": "Această setare permite utilizarea impersonării în principiu. Rețineți că impersonatorul trebuie să aibă și rolurile *_IMPERSONATOR atribuite corespunzător." + }, + "FEATURES": { + "LOGINDEFAULTORG": "Organizație implicită de conectare", + "LOGINDEFAULTORG_DESCRIPTION": "UI-ul de conectare va utiliza setările organizației implicite (și nu din instanță) dacă nu este setat niciun context de organizație", + "OIDCLEGACYINTROSPECTION": "Introspecție OIDC Legacy", + "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Am refactorizat recent endpointul de introspecție din motive de performanță. Această caracteristică poate fi utilizată pentru a reveni la implementarea legacy dacă apar erori neașteptate.", + "OIDCTOKENEXCHANGE": "Schimb de token OIDC", + "OIDCTOKENEXCHANGE_DESCRIPTION": "Activați tipul de grant experimental urn:ietf:params:oauth:grant-type:token-exchange pentru endpointul token OIDC. Schimbul de tokenuri poate fi utilizat pentru a solicita tokenuri cu o rază de acțiune mai mică sau pentru a impersona alți utilizatori. Consultați politica de securitate pentru a permite impersonarea pe o instanță.", + "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "Proiecții de introspecție OIDC Trigger", + "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Activați declanșatoarele de proiecție în timpul unei solicitări de introspecție. Acest lucru poate acționa ca o soluție dacă există probleme notabile de consistență în răspunsul de introspecție, dar poate avea un impact asupra performanței. Planificăm să eliminăm declanșatoarele pentru solicitările de introspecție în viitor.", + "USERSCHEMA": "Schema de utilizator", + "USERSCHEMA_DESCRIPTION": "Schemele de utilizator permit gestionarea schemelor de date ale utilizatorului. Dacă indicatorul este activat, veți putea utiliza noul API și caracteristicile sale.", + "ACTIONS": "Acțiuni", + "ACTIONS_DESCRIPTION": "Acțiunile v2 permit gestionarea execuțiilor și țintelor de date. Dacă indicatorul este activat, veți putea utiliza noul API și caracteristicile sale.", + "OIDCSINGLEV1SESSIONTERMINATION": "Terminarea sesiunii unice OIDC V1", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Dacă indicatorul este activat, veți putea termina o singură sesiune din UI-ul de conectare furnizând un id_token cu o revendicare sid ca id_token_hint pe endpointul end_session. Rețineți că în prezent toate sesiunile de la același agent utilizator (browser) sunt terminate în UI-ul de conectare. Sesiunile gestionate prin API-ul Session permit deja terminarea sesiunilor individuale.", + "DEBUGOIDCPARENTERROR": "Eroare de Depanare Părinte OIDC", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Dacă steagul este activat, eroarea părinte OIDC va fi înregistrată în consolă.", + "DISABLEUSERTOKENEVENT": "Dezactivează Evenimentul Token Utilizator", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Activează Logout Backchannel", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Logout-ul Back-Channel implementează OpenID Connect Back-Channel Logout 1.0 și poate fi folosit pentru a notifica clienții despre terminarea sesiunii la Producătorul OpenID.", + "PERMISSIONCHECKV2": "Verificare Permisiuni V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Dacă steagul este activat, veți putea folosi noua API și funcțiile sale.", + "WEBKEY": "Cheie Web", + "WEBKEY_DESCRIPTION": "Dacă steagul este activat, veți putea folosi noua API și funcțiile sale.", + "STATES": { + "INHERITED": "Moșteniți", + "ENABLED": "Activat", + "DISABLED": "Dezactivat" + }, + "INHERITED_DESCRIPTION": "Aceasta setează valoarea la valoarea implicită a sistemului.", + "INHERITEDINDICATOR_DESCRIPTION": { + "ENABLED": "\"Activat\" este moștenit", + "DISABLED": "\"Dezactivat\" este moștenit" + }, + "RESET": "Setați totul pentru a moșteni", + "CONSOLEUSEV2USERAPI": "Utilizați API-ul V2 în Consola pentru crearea utilizatorului", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Când acest indicator este activat, consola utilizează API-ul de utilizator V2 pentru a crea utilizatori noi. Cu API-ul V2, utilizatorii nou creați încep fără o stare inițială.", + "LOGINV2": "Autentificare V2", + "LOGINV2_DESCRIPTION": "Activarea acestei opțiuni pornește noua interfață de autentificare bazată pe TypeScript, cu securitate, performanță și personalizare îmbunătățite.", + "LOGINV2_BASEURI": "URI de bază" + }, + "DIALOG": { + "RESET": { + "DEFAULTTITLE": "Resetați setarea", + "DEFAULTDESCRIPTION": "Sunteți pe cale să vă resetați setările la configurația implicită a instanței dvs. Sigur doriți să continuați?", + "LOGINPOLICY_DESCRIPTION": "Avertisment: dacă continuați, setările furnizorului de identitate vor fi resetate și la setările instanței." + } + } + }, + "POLICY": { + "APPLIEDTO": "Aplicat la", + "PWD_COMPLEXITY": { + "TITLE": "Complexitatea parolei", + "DESCRIPTION": "Asigură că toate parolele setate corespund unui model specific", + "SYMBOLANDNUMBERERROR": "Trebuie să conțină o cifră și un simbol/semn de punctuație.", + "SYMBOLERROR": "Trebuie să includă un simbol/semn de punctuație.", + "NUMBERERROR": "Trebuie să includă o cifră.", + "PATTERNERROR": "Parola nu respectă modelul cerut." + }, + "NOTIFICATION": { + "TITLE": "Notificare", + "DESCRIPTION": "Determină la ce modificări vor fi trimise notificări.", + "PASSWORDCHANGE": "Modificarea parolei" + }, + "PRIVATELABELING": { + "DESCRIPTION": "Oferiți conectării stilul dvs. personalizat și modificați-i comportamentul.", + "PREVIEW_DESCRIPTION": "Modificările politicii vor fi implementate automat în mediul de previzualizare.", + "BTN": "Selectați fișier", + "ACTIVATEPREVIEW": "Aplicați configurația", + "DARK": "Mod întunecat", + "LIGHT": "Mod luminos", + "CHANGEVIEW": "Modificați vizualizarea", + "ACTIVATED": "Modificările politicii sunt acum LIVE", + "THEME": "Temă", + "COLORS": "Culori", + "FONT": "Font", + "ADVANCEDBEHAVIOR": "Comportament avansat", + "DROP": "Trageți imaginea aici sau", + "RELEASE": "Eliberați", + "DROPFONT": "Trageți fișierul fontului aici", + "RELEASEFONT": "Eliberați", + "USEOFLOGO": "Logo-ul dvs. va fi utilizat în conectare, precum și în e-mailuri, în timp ce pictograma este utilizată pentru elemente UI mai mici, cum ar fi în comutatorul organizației în consolă", + "MAXSIZE": "Dimensiunea maximă este limitată la 524 kB", + "EMAILNOSVG": "Formatul de fișier SVG nu este acceptat în e-mailuri. Prin urmare, încărcați-vă logo-ul în PNG sau în alt format acceptat.", + "MAXSIZEEXCEEDED": "Dimensiune maximă de 524 kB depășită.", + "NOSVGSUPPORTED": "SVG-urile nu sunt acceptate!", + "FONTINLOGINONLY": "Fontul este afișat în prezent numai în interfața de conectare.", + "BACKGROUNDCOLOR": "Culoare de fundal", + "PRIMARYCOLOR": "Culoare primară", + "WARNCOLOR": "Culoare de avertisment", + "FONTCOLOR": "Culoare font", + "VIEWS": { + "PREVIEW": "Previzualizare", + "CURRENT": "Configurația curentă" + }, + "PREVIEW": { + "TITLE": "Conectare", + "SECOND": "conectați-vă cu contul dvs. ZITADEL.", + "ERROR": "Utilizatorul nu a putut fi găsit!", + "PRIMARYBUTTON": "următorul", + "SECONDARYBUTTON": "înregistrați-vă" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "Mod automat", + "THEME_MODE_LIGHT": "Numai mod luminos", + "THEME_MODE_DARK": "Numai mod întunecat" + } + }, + "PWD_AGE": { + "TITLE": "Expirarea parolei", + "DESCRIPTION": "Puteți seta o politică pentru expirarea parolelor. Această politică va obliga utilizatorul să își schimbe parola la următoarea conectare după expirare. Nu există avertismente și notificări automate." + }, + "PWD_LOCKOUT": { + "TITLE": "Politica de blocare", + "DESCRIPTION": "Setați un număr maxim de încercări de parolă, după care conturile vor fi blocate." + }, + "PRIVATELABELING_POLICY": { + "TITLE": "Branding", + "BTN": "Selectați fișier", + "DESCRIPTION": "Personalizați aspectul conectării", + "ACTIVATEPREVIEW": "Activați configurația" + }, + "LOGIN_POLICY": { + "TITLE": "Setări de conectare", + "DESCRIPTION": "Definiți modul în care utilizatorii pot fi autentificați și configurați furnizorii de identitate", + "DESCRIPTIONCREATEADMIN": "Utilizatorii pot alege dintre furnizorii de identitate disponibili de mai jos.", + "DESCRIPTIONCREATEMGMT": "Utilizatorii pot alege dintre furnizorii de identitate disponibili de mai jos. Notă: Puteți utiliza atât furnizorii setați de sistem, cât și furnizorii setați numai pentru organizația dvs.", + "LIFETIME_INVALID": "Formularul conține valori nevalide.", + "SAVED": "Salvat cu succes!", + "PROVIDER_ADDED": "Furnizorul de identitate a fost activat." + }, + "PRIVACY_POLICY": { + "DESCRIPTION": "Setați linkurile către Politica dvs. de confidențialitate și Termenii și condițiile", + "TOSLINK": "Link către Termenii și condițiile", + "POLICYLINK": "Link către Politica de confidențialitate", + "HELPLINK": "Link către Ajutor", + "SUPPORTEMAIL": "E-mail de asistență", + "DOCSLINK": "Link către Documente (Consolă)", + "CUSTOMLINK": "Link personalizat (Consolă)", + "CUSTOMLINKTEXT": "Text link personalizat (Consolă)", + "SAVED": "Salvat cu succes!", + "RESET_TITLE": "Restabiliți valorile implicite", + "RESET_DESCRIPTION": "Sunteți pe cale să restabiliți linkurile implicite pentru TOS și Politica de confidențialitate. Doriți cu adevărat să continuați?" + }, + "LOGIN_TEXTS": { + "TITLE": "Texte interfață de conectare", + "DESCRIPTION": "Definiți-vă textele pentru interfețele de conectare. Dacă textele sunt goale, va fi utilizată valoarea implicită afișată ca substituent.", + "DESCRIPTION_SHORT": "Definiți-vă textele pentru interfețele de conectare.", + "NEWERVERSIONEXISTS": "Există o versiune mai nouă", + "CURRENTDATE": "Configurația curentă", + "CHANGEDATE": "Versiune mai nouă de la", + "KEYNAME": "Ecran / Interfață de conectare", + "RESET_TITLE": "Restabiliți valorile implicite", + "RESET_DESCRIPTION": "Sunteți pe cale să restabiliți toate valorile implicite. Toate modificările pe care le-ați făcut vor fi șterse definitiv. Doriți cu adevărat să continuați?", + "UNSAVED_TITLE": "Continuați fără a salva?", + "UNSAVED_DESCRIPTION": "Ați făcut modificări fără a salva. Doriți să salvați acum?", + "ACTIVE_LANGUAGE_NOT_ALLOWED": "Ați selectat o limbă care nu este permisă. Puteți continua să modificați textele. Dar dacă doriți ca utilizatorii dvs. să poată utiliza efectiv această limbă, modificați restricțiile instanței dvs.", + "LANGUAGES_NOT_ALLOWED": "Nepermise:", + "LANGUAGE": "Limbă", + "LANGUAGES": { + "de": "Deutsch", + "en": "English", + "es": "Español", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", + "pl": "Polski", + "zh": "简体中文", + "bg": "Български", + "pt": "Portuguese", + "mk": "Македонски", + "cs": "Čeština", + "ru": "Русский", + "nl": "Nederlands", + "sv": "Svenska", + "id": "Bahasa Indonesia", + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" + }, + "KEYS": { + "emailVerificationDoneText": "Verificarea e-mailului efectuată", + "emailVerificationText": "Verificarea e-mailului", + "externalUserNotFoundText": "Utilizatorul extern nu a fost găsit", + "footerText": "Subsol", + "initMfaDoneText": "MFA inițializat efectuată", + "initMfaOtpText": "Inițializați MFA", + "initMfaPromptText": "Solicitare inițializare MFA", + "initMfaU2fText": "Inițializați al doilea factor universal", + "initPasswordDoneText": "Parola inițializată efectuată", + "initPasswordText": "Inițializați parola", + "initializeDoneText": "Utilizatorul inițializat efectuat", + "initializeUserText": "Inițializați utilizatorul", + "linkingUserDoneText": "Utilizatorul de conectare efectuat", + "loginText": "Conectare", + "logoutText": "Deconectare", + "mfaProvidersText": "Furnizori MFA", + "passwordChangeDoneText": "Modificarea parolei efectuată", + "passwordChangeText": "Modificarea parolei", + "passwordResetDoneText": "Resetarea parolei efectuată", + "passwordText": "Parolă", + "registrationOptionText": "Opțiuni de înregistrare", + "registrationOrgText": "Înregistrați organizația", + "registrationUserText": "Înregistrați utilizatorul", + "selectAccountText": "Selectați contul", + "successLoginText": "Conectare cu succes", + "usernameChangeDoneText": "Modificarea numelui de utilizator efectuată", + "usernameChangeText": "Modificarea numelui de utilizator", + "verifyMfaOtpText": "Verificați OTP", + "verifyMfaU2fText": "Verificați al doilea factor universal", + "passwordlessPromptText": "Solicitare fără parolă", + "passwordlessRegistrationDoneText": "Înregistrarea fără parolă efectuată", + "passwordlessRegistrationText": "Înregistrarea fără parolă", + "passwordlessText": "Fără parolă", + "externalRegistrationUserOverviewText": "Prezentare generală utilizator înregistrare externă" + } + }, + "MESSAGE_TEXTS": { + "TYPE": "Notificare", + "TYPES": { + "INIT": "Inițializare", + "VE": "Verificați e-mailul", + "VP": "Verificați telefonul", + "VSO": "Verificați OTP SMS", + "VEO": "Verificați OTP e-mail", + "PR": "Resetare parolă", + "DC": "Revendicare domeniu", + "PL": "Fără parolă", + "PC": "Modificare parolă", + "IU": "Invitați utilizatorul" + }, + "CHIPS": { + "firstname": "Prenume", + "lastname": "Nume de familie", + "code": "Cod", + "preferredLoginName": "Nume de conectare preferat", + "displayName": "Nume afișat", + "nickName": "Poreclă", + "loginnames": "Nume de conectare", + "domain": "Domeniu", + "lastEmail": "Ultimul e-mail", + "lastPhone": "Ultimul telefon", + "verifiedEmail": "E-mail verificat", + "verifiedPhone": "Telefon verificat", + "changedate": "Data modificării", + "username": "Nume de utilizator", + "tempUsername": "Nume de utilizator temporar", + "otp": "Parolă unică", + "verifyUrl": "Verificați adresa URL a parolei unice", + "expiry": "Expirare", + "applicationName": "Numele aplicației" + }, + "TOAST": { + "UPDATED": "Textele personalizate au fost salvate." + } + }, + "DEFAULTLABEL": "Setările curente corespund standardului instanței dvs.", + "BTN_INSTALL": "Configurați", + "BTN_EDIT": "Modificați", + "DATA": { + "DESCRIPTION": "Descriere", + "MINLENGTH": "trebuie să aibă o lungime minimă", + "HASNUMBER": "trebuie să includă un număr", + "HASSYMBOL": "trebuie să includă un simbol", + "HASLOWERCASE": "trebuie să includă o literă mică", + "HASUPPERCASE": "trebuie să includă o literă mare", + "SHOWLOCKOUTFAILURES": "afișați erorile de blocare", + "MAXPASSWORDATTEMPTS": "Numărul maxim de încercări de parolă", + "MAXOTPATTEMPTS": "Numărul maxim de încercări OTP", + "EXPIREWARNDAYS": "Avertisment de expirare după zile", + "MAXAGEDAYS": "Valabilitate maximă în zile", + "USERLOGINMUSTBEDOMAIN": "Adăugați domeniul organizației ca sufix la numele de conectare", + "USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Dacă activați această setare, toate numele de conectare vor fi sufixate cu domeniul organizației. Dacă această setare este dezactivată, trebuie să vă asigurați că numele de utilizator sunt unice pentru toate organizațiile.", + "VALIDATEORGDOMAINS": "Verificarea domeniului organizației este necesară (provocare DNS sau HTTP)", + "SMTPSENDERADDRESSMATCHESINSTANCEDOMAIN": "Adresa expeditorului SMTP se potrivește cu domeniul instanței", + "ALLOWUSERNAMEPASSWORD_DESC": "Conectarea convențională cu nume de utilizator și parolă este permisă.", + "ALLOWEXTERNALIDP_DESC": "Conectarea este permisă pentru furnizorii de identitate de bază", + "ALLOWREGISTER_DESC": "Dacă opțiunea este selectată, în procesul de conectare apare un pas suplimentar pentru înregistrarea unui utilizator.", + "FORCEMFA": "Forțați MFA pentru toți utilizatorii", + "FORCEMFALOCALONLY": "Forțați MFA numai pentru utilizatorii autentificați local", + "FORCEMFALOCALONLY_DESC": "Dacă opțiunea este selectată, utilizatorii autentificați local trebuie să configureze un al doilea factor pentru conectare.", + "HIDEPASSWORDRESET_DESC": "Dacă opțiunea este selectată, utilizatorul nu își poate reseta parola în procesul de conectare.", + "HIDELOGINNAMESUFFIX": "Ascundeți sufixul numelui de conectare", + "HIDELOGINNAMESUFFIX_DESC": "Ascunde sufixul numelui de conectare în interfața de conectare", + "IGNOREUNKNOWNUSERNAMES_DESC": "Dacă opțiunea este selectată, ecranul cu parolă va fi afișat în procesul de conectare chiar dacă utilizatorul nu a fost găsit. Eroarea la verificarea parolei nu va dezvălui dacă numele de utilizator sau parola au fost greșite.", + "ALLOWDOMAINDISCOVERY_DESC": "Dacă opțiunea este selectată, sufixul (@domain.com) al unei intrări de nume de utilizator necunoscut pe ecranul de conectare va fi comparat cu domeniile organizației și va redirecționa către înregistrarea organizației respective în caz de succes.", + "DEFAULTREDIRECTURI": "URI implicit de redirecționare", + "DEFAULTREDIRECTURI_DESC": "Definește unde va fi redirecționat utilizatorul dacă conectarea a început fără un context de aplicație (de exemplu, din e-mail)", + "ERRORMSGPOPUP": "Afișați mesajul de eroare într-o casetă de dialog", + "DISABLEWATERMARK": "Ascundeți filigranul", + "DISABLEWATERMARK_DESC": "Ascundeți filigranul Alimentat de ZITADEL în interfața de conectare" + }, + "RESET": "Resetați la valoarea implicită a instanței", + "CREATECUSTOM": "Creați o politică personalizată", + "TOAST": { + "SET": "Politica a fost setată cu succes!", + "RESETSUCCESS": "Politica a fost resetată cu succes!", + "UPLOADSUCCESS": "Încărcat cu succes!", + "DELETESUCCESS": "Șters cu succes!", + "UPLOADFAILED": "Încărcarea a eșuat!" + } + }, + "ORG_DETAIL": { + "TITLE": "Organizație", + "DESCRIPTION": "Aici puteți edita configurația organizației dvs. și puteți gestiona membrii.", + "DETAIL": { + "TITLE": "Detalii", + "NAME": "Nume", + "DOMAIN": "Domeniu", + "STATE": { + "0": "Nedefinit", + "1": "Activ", + "2": "Inactiv" + } + }, + "MEMBER": { + "TITLE": "Membri", + "USERNAME": "Nume de utilizator", + "DISPLAYNAME": "Nume afișat", + "LOGINNAME": "Nume de conectare", + "EMAIL": "E-mail", + "ROLES": "Roluri", + "ADD": "Adăugați membru", + "ADDDESCRIPTION": "Introduceți numele utilizatorilor care urmează să fie adăugați." + }, + "TABLE": { + "TOTAL": "Număr total de intrări", + "SELECTION": "Elemente selectate", + "DEACTIVATE": "Dezactivați utilizatorul", + "ACTIVATE": "Activați utilizatorul", + "DELETE": "Ștergeți utilizatorul", + "CLEAR": "Goliți selecția" + } + }, + "PROJECT": { + "PAGES": { + "TITLE": "Proiect", + "DESCRIPTION": "Aici puteți defini aplicații, puteți gestiona rolurile și puteți acorda altor organizații să utilizeze proiectul dvs.", + "DELETE": "Ștergeți proiectul", + "DETAIL": "Detalii", + "CREATE": "Creați proiectul", + "CREATE_DESC": "Introduceți numele proiectului dvs.", + "ROLE": "Rol", + "NOITEMS": "Niciun proiect", + "ZITADELPROJECT": "Acesta aparține proiectului ZITADEL. Atenție: Dacă faceți modificări, ZITADEL ar putea să nu se comporte conform intențiilor.", + "TYPE": { + "OWNED": "Proiecte deținute", + "OWNED_SINGULAR": "Proiect deținut", + "GRANTED_SINGULAR": "Proiect acordat al {{name}}" + }, + "PRIVATELABEL": { + "TITLE": "Setare de branding", + "0": { + "TITLE": "Nespecificat", + "DESC": "De îndată ce utilizatorul este identificat, brandingul organizației utilizatorului identificat va fi afișat înainte ca valoarea implicită a sistemului să fie afișată." + }, + "1": { + "TITLE": "Utilizați setarea proiectului", + "DESC": "Va fi afișat brandingul organizației care deține proiectul" + }, + "2": { + "TITLE": "Utilizați setarea Organizației utilizatorului", + "DESC": "Va fi afișat brandingul organizației proiectului, dar de îndată ce utilizatorul este identificat, va fi afișată setarea organizației utilizatorului identificat." + }, + "DIALOG": { + "TITLE": "Setare branding", + "DESCRIPTION": "Selectați comportamentul conectării, atunci când utilizați proiectul." + } + }, + "PINNED": "Fixat", + "ALL": "Toate", + "CREATEDON": "Creat la", + "LASTMODIFIED": "Ultima modificare la", + "ADDNEW": "Creați un proiect nou", + "DIALOG": { + "REACTIVATE": { + "TITLE": "Reactivați proiectul", + "DESCRIPTION": "Doriți cu adevărat să vă reactivați proiectul?" + }, + "DEACTIVATE": { + "TITLE": "Dezactivați proiectul", + "DESCRIPTION": "Doriți cu adevărat să vă dezactivați proiectul?" + }, + "DELETE": { + "TITLE": "Ștergeți proiectul", + "DESCRIPTION": "Doriți cu adevărat să vă ștergeți proiectul?", + "TYPENAME": "Tastați numele proiectului pentru a-l șterge definitiv." + } + } + }, + "SETTINGS": { + "TITLE": "Setări", + "DESCRIPTION": "" + }, + "STATE": { + "TITLE": "Stare", + "0": "Nedefinit", + "1": "Activ", + "2": "Inactiv" + }, + "TYPE": { + "TITLE": "Tip", + "0": "Tip necunoscut", + "1": "Deținut", + "2": "Acordat" + }, + "NAME": "Nume", + "NAMEDIALOG": { + "TITLE": "Redenumiți proiectul", + "DESCRIPTION": "Introduceți noul nume pentru proiectul dvs.", + "NAME": "Nume nou" + }, + "MEMBER": { + "TITLE": "Manageri", + "TITLEDESC": "Managerii pot face modificări acestui proiect pe baza rolului lor.", + "DESCRIPTION": "Acești manageri ar putea să vă poată edita proiectul.", + "USERNAME": "Nume de utilizator", + "DISPLAYNAME": "Nume afișat", + "LOGINNAME": "Nume de conectare", + "EMAIL": "E-mail", + "ROLES": "Roluri", + "USERID": "ID utilizator" + }, + "GRANT": { + "EMPTY": "Nicio organizație acordată.", + "TITLE": "Granturi de proiect", + "DESCRIPTION": "Permiteți altei organizații să vă utilizeze proiectul.", + "EDITTITLE": "Editați rolurile", + "CREATE": { + "TITLE": "Creați un grant de organizație", + "SEL_USERS": "Selectați utilizatorii cărora doriți să le acordați acces", + "SEL_PROJECT": "Căutați un proiect", + "SEL_ROLES": "Selectați rolurile pe care doriți să le adăugați la grant", + "SEL_USER": "Selectați utilizatori", + "SEL_ORG": "Căutați o organizație", + "SEL_ORG_DESC": "Căutați organizația căreia să acordați.", + "ORG_DESCRIPTION": "Sunteți pe cale să acordați un utilizator pentru organizația {{name}}.", + "ORG_DESCRIPTION_DESC": "Schimbați contextul în antetul de mai sus pentru a acorda un utilizator pentru o altă organizație.", + "SEL_ORG_FORMFIELD": "Organizație", + "FOR_ORG": "Grantul este creat pentru:" + }, + "DETAIL": { + "TITLE": "Grant de proiect", + "DESC": "Puteți selecta ce roluri pot fi utilizate de organizația specificată și puteți alege manageri", + "MEMBERTITLE": "Manageri", + "MEMBERDESC": "Aceștia sunt managerii organizației acordate. Adăugați aici utilizatori care ar trebui să obțină acces pentru a edita datele proiectului.", + "PROJECTNAME": "Numele proiectului", + "GRANTEDORG": "Organizație acordată", + "RESOURCEOWNER": "Proprietarul resursei" + }, + "STATE": "Stare", + "STATES": { + "1": "Activ", + "2": "Inactiv" + }, + "ALL": "Toate", + "SHOWDETAIL": "Afișați detalii", + "USER": "Utilizator", + "MEMBERS": "Manageri", + "ORG": "Organizație", + "PROJECTNAME": "Numele proiectului", + "GRANTEDORG": "Organizație acordată", + "GRANTEDORGDOMAIN": "Domeniu", + "RESOURCEOWNER": "Proprietarul resursei", + "GRANTEDORGNAME": "Nume organizație", + "GRANTID": "Id-ul grantului", + "CREATIONDATE": "Data creării", + "CHANGEDATE": "Ultima modificare", + "DATES": "Date", + "ROLENAMESLIST": "Roluri", + "NOROLES": "Niciun rol", + "TYPE": "Tip", + "TOAST": { + "PROJECTGRANTUSERGRANTADDED": "Grantul de proiect a fost creat.", + "PROJECTGRANTADDED": "Grantul de proiect a fost creat.", + "PROJECTGRANTCHANGED": "Grantul de proiect a fost modificat.", + "PROJECTGRANTMEMBERADDED": "Managerul grantului a fost adăugat.", + "PROJECTGRANTMEMBERCHANGED": "Managerul grantului a fost modificat.", + "PROJECTGRANTMEMBERREMOVED": "Managerul grantului a fost eliminat.", + "PROJECTGRANTUPDATED": "Grantul de proiect a fost actualizat" + }, + "DIALOG": { + "DELETE_TITLE": "Ștergeți grantul de proiect", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți un grant de proiect. Sigur doriți să continuați?" + }, + "ROLES": "Roluri de proiect" + }, + "APP": { + "TITLE": "Aplicații", + "NAME": "Nume", + "NAMEREQUIRED": "Este necesar un nume." + }, + "ROLE": { + "EMPTY": "Niciun rol nu a fost creat încă.", + "ADDNEWLINE": "Adăugați un rol suplimentar", + "KEY": "Cheie", + "TITLE": "Roluri", + "DESCRIPTION": "Definiți câteva roluri care pot fi utilizate pentru a crea granturi de proiect.", + "NAME": "Nume", + "DISPLAY_NAME": "Nume afișat", + "GROUP": "Grup", + "ACTIONS": "Acțiuni", + "ADDTITLE": "Creați rolul", + "ADDDESCRIPTION": "Introduceți datele pentru noul rol.", + "EDITTITLE": "Editați rolul", + "EDITDESCRIPTION": "Introduceți datele noi pentru rol.", + "DELETE": "Ștergeți rolul", + "CREATIONDATE": "Creat", + "CHANGEDATE": "Ultima modificare", + "SELECTGROUPTOOLTIP": "Selectați toate rolurile grupului {{group}}.", + "OPTIONS": "Opțiuni", + "ASSERTION": "Afirmați rolurile la autentificare", + "ASSERTION_DESCRIPTION": "Informațiile despre roluri sunt trimise din endpoint-ul Userinfo și, în funcție de setările aplicației dvs., în tokenuri și alte tipuri.", + "CHECK": "Verificați autorizarea la autentificare", + "CHECK_DESCRIPTION": "Dacă este setat, utilizatorilor li se permite să se autentifice numai dacă oricărui cont le este atribuit vreun rol.", + "DIALOG": { + "DELETE_TITLE": "Ștergeți rolul", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți un rol de proiect. Sigur doriți să continuați?" + } + }, + "HAS_PROJECT": "Verificați proiectul la autentificare", + "HAS_PROJECT_DESCRIPTION": "Se verifică dacă organizația utilizatorului are acest proiect. Dacă nu, utilizatorul nu se poate autentifica.", + "TABLE": { + "TOTAL": "Numărul total de intrări:", + "SELECTION": "Elemente selectate", + "DEACTIVATE": "Dezactivați proiectul", + "ACTIVATE": "Activați proiectul", + "DELETE": "Ștergeți proiectul", + "ORGNAME": "Nume organizație", + "ORGDOMAIN": "Domeniu organizație", + "STATE": "Stare", + "TYPE": "Tip", + "CREATIONDATE": "Creat la", + "CHANGEDATE": "Ultima modificare", + "RESOURCEOWNER": "Proprietar", + "SHOWTABLE": "Afișați tabelul", + "SHOWGRID": "Afișați grila", + "EMPTY": "Niciun proiect găsit" + }, + "TOAST": { + "MEMBERREMOVED": "Manager eliminat.", + "MEMBERSADDED": "Manageri adăugați.", + "MEMBERADDED": "Manager adăugat.", + "MEMBERCHANGED": "Manager modificat.", + "ROLESCREATED": "Roluri create.", + "ROLEREMOVED": "Rol eliminat.", + "ROLECHANGED": "Rol modificat.", + "REACTIVATED": "Reactivat.", + "DEACTIVATED": "Dezactivat.", + "CREATED": "Proiect creat.", + "UPDATED": "Proiect modificat.", + "GRANTUPDATED": "Grant modificat.", + "DELETED": "Proiect șters." + } + }, + "ROLES": { + "DIALOG": { + "DELETE_TITLE": "Ștergeți rolul", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți un rol. Sigur doriți să continuați?" + } + }, + "NEXTSTEPS": { + "TITLE": "Pași următori" + }, + "IDP": { + "LIST": { + "ACTIVETITLE": "Furnizori de identitate activi" + }, + "CREATE": { + "TITLE": "Adăugați furnizor", + "DESCRIPTION": "Selectați unul sau mai mulți dintre următorii furnizori.", + "STEPPERTITLE": "Creați furnizorul", + "OIDC": { + "TITLE": "Furnizor OIDC", + "DESCRIPTION": "Introduceți datele necesare pentru furnizorul dvs. OIDC." + }, + "OAUTH": { + "TITLE": "Furnizor OAuth", + "DESCRIPTION": "Introduceți datele necesare pentru furnizorul dvs. OAuth." + }, + "JWT": { + "TITLE": "Furnizor JWT", + "DESCRIPTION": "Introduceți datele necesare pentru furnizorul dvs. JWT." + }, + "GOOGLE": { + "TITLE": "Furnizor Google", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. de identitate Google" + }, + "GITLAB": { + "TITLE": "Furnizor Gitlab", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. de identitate Gitlab" + }, + "GITLABSELFHOSTED": { + "TITLE": "Furnizor Gitlab autogăzduit", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. de identitate Gitlab autogăzduit" + }, + "GITHUBES": { + "TITLE": "Furnizor GitHub Enterprise Server", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. de identitate GitHub Enterprise Server" + }, + "GITHUB": { + "TITLE": "Furnizor Github", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. de identitate Github" + }, + "AZUREAD": { + "TITLE": "Furnizor Microsoft", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. de identitate Microsoft" + }, + "LDAP": { + "TITLE": "Active Directory / LDAP", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. LDAP" + }, + "APPLE": { + "TITLE": "Conectare cu Apple", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. Apple" + }, + "SAML": { + "TITLE": "Conectare cu SAML", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. SAML" + } + }, + "DETAIL": { + "TITLE": "Furnizor de identitate", + "DESCRIPTION": "Actualizați configurația furnizorului dvs.", + "DATECREATED": "Creat", + "DATECHANGED": "Modificat" + }, + "OPTIONS": { + "ISAUTOCREATION": "Creare automată", + "ISAUTOCREATION_DESC": "Dacă este selectat, va fi creat un cont dacă nu există încă.", + "ISAUTOUPDATE": "Actualizare automată", + "ISAUTOUPDATE_DESC": "Dacă este selectat, conturile sunt actualizate la reautentificare.", + "ISCREATIONALLOWED": "Crearea contului permisă (manual)", + "ISCREATIONALLOWED_DESC": "Determină dacă conturile pot fi create folosind un cont extern. Dezactivați dacă utilizatorii nu ar trebui să poată edita informațiile contului când auto_creation este activată.", + "ISLINKINGALLOWED": "Conectarea contului permisă (manual)", + "ISLINKINGALLOWED_DESC": "Determină dacă o identitate poate fi conectată manual la un cont existent. Dezactivați dacă utilizatorilor ar trebui să li se permită să conecteze doar contul propus în caz de auto_linking activ.", + "AUTOLINKING_DESC": "Determină dacă unei identități i se va solicita să fie conectată la un cont existent.", + "AUTOLINKINGTYPE": { + "0": "Dezactivat", + "1": "Verificați dacă există un nume de utilizator existent", + "2": "Verificați dacă există un e-mail existent" + } + }, + "OWNERTYPES": { + "0": "necunoscut", + "1": "Instanță", + "2": "Organizație" + }, + "STATES": { + "1": "activ", + "2": "inactiv" + }, + "AZUREADTENANTTYPES": { + "3": "ID chiriaș", + "0": "Comun", + "1": "Organizații", + "2": "Consumatori" + }, + "AZUREADTENANTTYPE": "Tip chiriaș", + "AZUREADTENANTID": "ID chiriaș", + "EMAILVERIFIED": "E-mail verificat", + "NAMEHINT": "Dacă este specificat, va fi afișat în interfața de conectare.", + "OPTIONAL": "opțional", + "LDAPATTRIBUTES": "Atribute LDAP", + "UPDATEBINDPASSWORD": "actualizați parola de conectare", + "UPDATECLIENTSECRET": "actualizați secretul clientului", + "ADD": "Adăugați furnizor de identitate", + "TYPE": "Tip", + "OWNER": "Proprietar", + "ID": "ID", + "NAME": "Nume", + "AUTHORIZATIONENDPOINT": "Punct final de autorizare", + "TOKENENDPOINT": "Punct final de token", + "USERENDPOINT": "Punct final utilizator", + "IDATTRIBUTE": "Atribut ID", + "AVAILABILITY": "Disponibilitate", + "AVAILABLE": "disponibil", + "AVAILABLEBUTINACTIVE": "disponibil, dar inactiv", + "SETAVAILABLE": "setați ca disponibil", + "SETUNAVAILABLE": "setați ca indisponibil", + "CONFIG": "Configurație", + "STATE": "Stare", + "ISSUER": "Emitent", + "SCOPESLIST": "Listă de domenii", + "CLIENTID": "ID client", + "CLIENTSECRET": "Secret client", + "LDAPCONNECTION": "Conexiune", + "LDAPUSERBINDING": "Conectarea utilizatorului", + "BASEDN": "BaseDn", + "BINDDN": "BindDn", + "BINDPASSWORD": "Parola de conectare", + "SERVERS": "Servere", + "STARTTLS": "Porniți TLS", + "TIMEOUT": "Timeout în secunde", + "USERBASE": "Baza de utilizatori", + "USERFILTERS": "Filtre de utilizatori", + "USEROBJECTCLASSES": "Clase de obiecte utilizator", + "REQUIRED": "necesar", + "LDAPIDATTRIBUTE": "Atribut ID", + "AVATARURLATTRIBUTE": "Atribut adresa URL avatar", + "DISPLAYNAMEATTRIBUTE": "Atribut Nume afișat", + "EMAILATTRIBUTEATTRIBUTE": "Atribut Atribut e-mail", + "EMAILVERIFIEDATTRIBUTE": "Atribut E-mail verificat", + "FIRSTNAMEATTRIBUTE": "Atribut Prenume", + "LASTNAMEATTRIBUTE": "Atribut Nume de familie", + "NICKNAMEATTRIBUTE": "Atribut Poreclă", + "PHONEATTRIBUTE": "Atribut Telefon", + "PHONEVERIFIEDATTRIBUTE": "Atribut Telefon verificat", + "PREFERREDLANGUAGEATTRIBUTE": "Atribut Limbă preferată", + "PREFERREDUSERNAMEATTRIBUTE": "Atribut Nume de utilizator preferat", + "PROFILEATTRIBUTE": "Atribut Profil", + "IDPDISPLAYNAMMAPPING": "Mapare nume afișat IdP", + "USERNAMEMAPPING": "Mapare nume de utilizator", + "DATES": "Date", + "CREATIONDATE": "Creat la", + "CHANGEDATE": "Ultima modificare", + "DEACTIVATE": "Dezactivați", + "ACTIVATE": "Activați", + "DELETE": "Ștergeți", + "DELETE_TITLE": "Ștergeți IdP", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți un furnizor de identitate. Modificările rezultate sunt irevocabile. Doriți cu adevărat să faceți acest lucru?", + "REMOVE_WARN_TITLE": "Eliminați IdP", + "REMOVE_WARN_DESCRIPTION": "Sunteți pe cale să eliminați un furnizor de identitate. Aceasta va elimina selecția IdP-ului disponibil pentru utilizatorii dvs. și utilizatorii deja înregistrați nu se vor mai putea conecta. Sigur doriți să continuați?", + "DELETE_SELECTION_TITLE": "Ștergeți IdP", + "DELETE_SELECTION_DESCRIPTION": "Sunteți pe cale să ștergeți un furnizor de identitate. Modificările rezultate sunt irevocabile. Doriți cu adevărat să faceți acest lucru?", + "EMPTY": "Niciun IdP disponibil", + "OIDC": { + "GENERAL": "Informații generale", + "TITLE": "Configurație OIDC", + "DESCRIPTION": "Introduceți datele pentru furnizorul de identitate OIDC." + }, + "JWT": { + "TITLE": "Configurație JWT", + "DESCRIPTION": "Introduceți datele pentru furnizorul de identitate JWT.", + "HEADERNAME": "Nume antet", + "JWTENDPOINT": "Punct final JWT", + "JWTKEYSENDPOINT": "Punct final chei JWT" + }, + "APPLE": { + "TEAMID": "ID echipă", + "KEYID": "ID cheie", + "PRIVATEKEY": "Cheie privată", + "UPDATEPRIVATEKEY": "Actualizați cheia privată", + "UPLOADPRIVATEKEY": "Încărcați cheia privată", + "KEYMAXSIZEEXCEEDED": "Dimensiune maximă de 5 kB depășită." + }, + "SAML": { + "METADATAXML": "XML metadata", + "METADATAURL": "URL metadata", + "BINDING": "Legare", + "SIGNEDREQUEST": "Solicitare semnată", + "NAMEIDFORMAT": "Format ID nume", + "TRANSIENTMAPPINGATTRIBUTENAME": "Nume atribut mapare personalizat", + "TRANSIENTMAPPINGATTRIBUTENAME_DESC": "Nume alternativ de atribut pentru a mapa utilizatorul în cazul în care nameid-format returnat este tranzitoriu, de exemplu, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" + }, + "TOAST": { + "SAVED": "Salvat cu succes.", + "REACTIVATED": "Idp reactivat.", + "DEACTIVATED": "Idp dezactivat.", + "SELECTEDREACTIVATED": "Idp-urile selectate au fost reactivate.", + "SELECTEDDEACTIVATED": "Idp-urile selectate au fost dezactivate.", + "SELECTEDKEYSDELETED": "Idp-urile selectate au fost șterse.", + "DELETED": "Idp eliminat cu succes!", + "ADDED": "Adăugat cu succes.", + "REMOVED": "Eliminat cu succes." + }, + "ISIDTOKENMAPPING": "Mapați din tokenul ID", + "ISIDTOKENMAPPING_DESC": "Dacă este selectat, informațiile despre furnizor sunt mapate din tokenul ID, nu din endpoint-ul userinfo." + }, + "MFA": { + "LIST": { + "MULTIFACTORTITLE": "Fără parolă", + "MULTIFACTORDESCRIPTION": "Definiți-vă multifactorii pentru autentificarea fără parolă aici.", + "SECONDFACTORTITLE": "Autentificare multifactor", + "SECONDFACTORDESCRIPTION": "Definiți alți factori posibili cu care vă puteți securiza autentificarea cu parolă." + }, + "CREATE": { + "TITLE": "Factor nou", + "DESCRIPTION": "Selectați noul dvs. tip de factor." + }, + "DELETE": { + "TITLE": "Ștergeți factorul", + "DESCRIPTION": "Sunteți pe cale să ștergeți un factor din Setările de conectare. Sigur doriți să continuați?" + }, + "TOAST": { + "ADDED": "Adăugat cu succes.", + "SAVED": "Salvat cu succes.", + "DELETED": "Eliminat cu succes" + }, + "TYPE": "Tip", + "MULTIFACTORTYPES": { + "0": "Necunoscut", + "1": "Amprentă, chei de securitate, Face ID și altele" + }, + "SECONDFACTORTYPES": { + "0": "Necunoscut", + "1": "Parolă unică după aplicația de autentificare (TOTP)", + "2": "Amprentă, chei de securitate, Face ID și altele", + "3": "Parolă unică prin e-mail (OTP e-mail)", + "4": "Parolă unică prin SMS (OTP SMS)" + } + }, + "LOGINPOLICY": { + "CREATE": { + "TITLE": "Setări de conectare", + "DESCRIPTION": "Definiți modul în care utilizatorii dvs. pot fi autentificați în organizația dvs." + }, + "IDPS": "Furnizori de identitate", + "ADDIDP": { + "TITLE": "Adăugați furnizor de identate", + "DESCRIPTION": "Puteți selecta furnizori predefiniți sau autocreați pentru autentificare.", + "SELECTIDPS": "Furnizori de identitate" + }, + "PASSWORDLESS": "Conectare fără parolă", + "PASSWORDLESSTYPE": { + "0": "Nepermis", + "1": "Permis" + } + }, + "SMTP": { + "LIST": { + "TITLE": "Furnizor SMTP", + "DESCRIPTION": "Aceștia sunt furnizorii SMTP pentru instanța dvs. ZITADEL. Activați-l pe cel pe care doriți să-l utilizați pentru a trimite notificări utilizatorilor dvs.", + "EMPTY": "Niciun furnizor SMTP disponibil", + "ACTIVATED": "Activat", + "ACTIVATE": "Activați furnizorul", + "DEACTIVATE": "Dezactivați furnizorul", + "TEST": "Testați-vă furnizorul", + "TYPE": "Tip", + "DIALOG": { + "ACTIVATED": "Configurația SMTP a fost activată", + "ACTIVATE_WARN_TITLE": "Activați configurația SMTP", + "ACTIVATE_WARN_DESCRIPTION": "Sunteți pe cale să activați o configurație SMTP. Mai întâi vom dezactiva furnizorul activ curent și apoi vom activa această configurație. Sigur doriți să continuați?", + "DEACTIVATE_WARN_TITLE": "Dezactivați configurația SMTP", + "DEACTIVATE_WARN_DESCRIPTION": "Sunteți pe cale să dezactivați o configurație SMTP. Sigur doriți să continuați?", + "DEACTIVATED": "Configurația SMTP a fost dezactivată", + "DELETE_TITLE": "Ștergeți configurația SMTP", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți o configurație. Confirmați această acțiune tastând numele expeditorului", + "DELETED": "Configurația SMTP a fost ștearsă", + "SENDER": "Tastați {{value}}, pentru a șterge această configurație SMTP.", + "TEST_TITLE": "Testați-vă configurația SMTP", + "TEST_DESCRIPTION": "Specificați o adresă de e-mail pentru a vă testa configurația SMTP pentru acest furnizor", + "TEST_EMAIL": "Adresă de e-mail", + "TEST_RESULT": "Rezultatul testului" + } + }, + "CREATE": { + "TITLE": "Adăugați furnizor SMTP", + "DESCRIPTION": "Selectați unul sau mai mulți dintre următorii furnizori.", + "STEPS": { + "TITLE": "Adăugați furnizor SMTP {{ value }}", + "CREATE_DESC_TITLE": "Introduceți pas cu pas setările dvs. SMTP {{ value }}", + "CURRENT_DESC_TITLE": "Acestea sunt setările dvs. SMTP", + "PROVIDER_SETTINGS": "Setări furnizor SMTP", + "SENDER_SETTINGS": "Setări expeditor", + "NEXT_STEPS": "Pași următori", + "ACTIVATE": { + "TITLE": "Activați-vă furnizorul SMTP", + "DESCRIPTION": "ZITADEL nu poate utiliza acest furnizor SMTP pentru a trimite notificări până când nu îl activați. Dacă activați acest furnizor, orice alt furnizor care era activ va fi acum dezactivat." + }, + "DEACTIVATE": { + "TITLE": "Dezactivați-vă furnizorul SMTP", + "DESCRIPTION": "Dacă dezactivați acest furnizor SMTP, ZITADEL nu îl poate utiliza pentru a trimite notificări până când nu îl activați din nou." + }, + "SAVE_SETTINGS": "Salvați-vă setările", + "TEST": { + "TITLE": "Testați-vă setările", + "DESCRIPTION": "Puteți testa setările furnizorului dvs. SMTP și puteți verifica rezultatul testului înainte de a le salva", + "RESULT": "E-mailul dvs. a fost trimis cu succes" + } + } + }, + "DETAIL": { + "TITLE": "Setări furnizor SMTP" + }, + "EMPTY": "Niciun furnizor SMTP disponibil", + "STEPS": { + "SENDGRID": {} + } + }, + "APP": { + "LIST": "Aplicații", + "COMPLIANCE": "Conformitate OIDC", + "URLS": "Adrese URL", + "CONFIGURATION": "Configurație", + "TOKEN": "Setări token", + "PAGES": { + "TITLE": "Aplicație", + "ID": "ID", + "DESCRIPTION": "Aici puteți edita datele aplicației și configurația acesteia.", + "CREATE": "Creați aplicația", + "CREATE_SELECT_PROJECT": "Selectați mai întâi proiectul dvs.", + "CREATE_NEW_PROJECT": "sau introduceți numele pentru noul dvs. proiect", + "CREATE_DESC_TITLE": "Introduceți pas cu pas detaliile aplicației dvs.", + "CREATE_DESC_SUB": "Va fi generată automat o configurație recomandată.", + "STATE": "Stare", + "DATECREATED": "Creat", + "DATECHANGED": "Modificat", + "URLS": "Adrese URL", + "DELETE": "Ștergeți aplicația", + "JUMPTOPROJECT": "Pentru a configura roluri, autorizații și multe altele, navigați la proiect.", + "DETAIL": { + "TITLE": "Detalii", + "STATE": { + "0": "Nedefinit", + "1": "Activ", + "2": "Inactiv" + } + }, + "DIALOG": { + "CONFIG": { + "TITLE": "Modificați configurația OIDC" + }, + "DELETE": { + "TITLE": "Ștergeți aplicația", + "DESCRIPTION": "Doriți cu adevărat să ștergeți această aplicație?" + } + }, + "NEXTSTEPS": { + "TITLE": "Pași următori", + "0": { + "TITLE": "Adăugați roluri", + "DESC": "Introduceți rolurile proiectului dvs." + }, + "1": { + "TITLE": "Adăugați utilizatori", + "DESC": "Adăugați utilizatori noi ai organizației dvs." + }, + "2": { + "TITLE": "Ajutor și asistență", + "DESC": "Citiți documentația noastră despre crearea aplicațiilor sau contactați asistența noastră" + } + } + }, + "NAMEDIALOG": { + "TITLE": "Redenumiți aplicația", + "DESCRIPTION": "Introduceți noul nume pentru aplicația dvs.", + "NAME": "Nume nou" + }, + "NAME": "Nume", + "TYPE": "Tipul aplicației", + "AUTHMETHOD": "Metoda de autentificare", + "AUTHMETHODSECTION": "Metoda de autentificare", + "GRANT": "Tipuri de grant", + "ADDITIONALORIGINS": "Origini suplimentare", + "ADDITIONALORIGINSDESC": "Dacă doriți să adăugați origini suplimentare la aplicația dvs. care nu sunt utilizate ca redirecționare, puteți face acest lucru aici.", + "ORIGINS": "Origini", + "NOTANORIGIN": "Valoarea introdusă nu este o origine", + "PROSWITCH": "Sunt profesionist. Omiteți acest vrăjitor.", + "NAMEANDTYPESECTION": "Nume și tip", + "TITLEFIRST": "Numele aplicației", + "TYPETITLE": "Tipul de aplicație", + "OIDC": { + "WELLKNOWN": "Puteți prelua linkuri suplimentare de la punctul final de descoperire.", + "INFO": { + "ISSUER": "Emitent", + "CLIENTID": "ID client" + }, + "CURRENT": "Configurația curentă", + "TOKENSECTIONTITLE": "Opțiuni AuthToken", + "REDIRECTSECTIONTITLE": "Setări de redirecționare", + "REDIRECTTITLE": "Specificați URI-urile unde se va redirecționa conectarea.", + "POSTREDIRECTTITLE": "Acesta este URI-ul de redirecționare după deconectare.", + "REDIRECTDESCRIPTIONWEB": "URI-urile de redirecționare trebuie să înceapă cu https://. http:// este valabil numai cu modul de dezvoltare activat.", + "REDIRECTDESCRIPTIONNATIVE": "URI-urile de redirecționare trebuie să înceapă cu propriul protocol, http://127.0.0.1, http://[::1] sau http://localhost.", + "REDIRECTNOTVALID": "Acest URI de redirecționare nu este valabil.", + "COMMAORENTERSEPERATION": "separați cu ↵", + "TYPEREQUIRED": "Tipul este necesar.", + "TITLE": "Configurație OIDC", + "CLIENTID": "ID client", + "CLIENTSECRET": "Secret client", + "CLIENTSECRET_NOSECRET": "Cu fluxul de autentificare ales, nu este necesar niciun secret și, prin urmare, nu este disponibil.", + "CLIENTSECRET_DESCRIPTION": "Păstrați secretul clientului într-un loc sigur, deoarece va dispărea odată ce dialogul este închis.", + "REGENERATESECRET": "Regenerați secretul clientului", + "DEVMODE": "Mod dezvoltare", + "DEVMODE_ENABLED": "Activat", + "DEVMODE_DISABLED": "Dezactivat", + "DEVMODEDESC": "Atenție: Cu modul de dezvoltare activat, URI-urile de redirecționare nu vor fi validate.", + "SKIPNATIVEAPPSUCCESSPAGE": "Omiteți pagina de succes la conectare", + "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Omiteți pagina de succes după o conectare pentru această aplicație nativă.", + "REDIRECT": "URI-uri de redirecționare", + "REDIRECTSECTION": "URI-uri de redirecționare", + "POSTLOGOUTREDIRECT": "URI-uri de redirecționare după deconectare", + "RESPONSESECTION": "Tipuri de răspuns", + "GRANTSECTION": "Tipuri de grant", + "GRANTTITLE": "Selectați tipurile de grant. Notă: Implicit este disponibil numai pentru aplicațiile bazate pe browser.", + "APPTYPE": { + "0": "Web", + "1": "Agent utilizator", + "2": "Nativ" + }, + "RESPONSETYPE": "Tipuri de răspuns", + "RESPONSE": { + "0": "Cod", + "1": "Token ID", + "2": "Token-Token ID" + }, + "REFRESHTOKEN": "Token de reîmprospătare", + "GRANTTYPE": "Tipuri de grant", + "GRANT": { + "0": "Cod de autorizare", + "1": "Implicit", + "2": "Token de reîmprospătare", + "3": "Cod dispozitiv", + "4": "Schimb token" + }, + "AUTHMETHOD": { + "0": "De bază", + "1": "Post", + "2": "Niciunul", + "3": "JWT cheie privată" + }, + "TOKENTYPE": "Tipul token-ului de autentificare", + "TOKENTYPE0": "Token Bearer", + "TOKENTYPE1": "JWT", + "UNSECUREREDIRECT": "Sper cu siguranță că știți ce faceți.", + "OVERVIEWSECTION": "Prezentare generală", + "OVERVIEWTITLE": "Ați terminat acum. Revizuiți configurația dvs.", + "ACCESSTOKENROLEASSERTION": "Adăugați rolurile utilizatorului la token-ul de acces", + "ACCESSTOKENROLEASSERTION_DESCRIPTION": "Dacă este selectat, rolurile solicitate ale utilizatorului autentificat sunt adăugate la token-ul de acces.", + "IDTOKENROLEASSERTION": "Roluri utilizator în interiorul token-ului ID", + "IDTOKENROLEASSERTION_DESCRIPTION": "Dacă este selectat, rolurile solicitate ale utilizatorului autentificat sunt adăugate la token-ul ID.", + "IDTOKENUSERINFOASSERTION": "Informații utilizator în interiorul token-ului ID", + "IDTOKENUSERINFOASSERTION_DESCRIPTION": "Permite clienților să preia revendicări de profil, e-mail, telefon și adresă din token-ul ID.", + "CLOCKSKEW": "Permite clienților să gestioneze decalajul de ceas al OP și al clientului. Durata (0-5s) va fi adăugată revendicării exp și scăzută din iats, auth_time și nbf.", + "RECOMMENDED": "recomandat", + "NOTRECOMMENDED": "nerecomandat", + "SELECTION": { + "APPTYPE": { + "WEB": { + "TITLE": "Web", + "DESCRIPTION": "Aplicații Web obișnuite precum .net, PHP, Node.js, Java etc." + }, + "NATIVE": { + "TITLE": "Nativ", + "DESCRIPTION": "Aplicații mobile, desktop, dispozitive inteligente etc." + }, + "USERAGENT": { + "TITLE": "Agent utilizator", + "DESCRIPTION": "Aplicații cu o singură pagină (SPA) și în general toate cadrele JS executate în browsere" + } + } + } + }, + "API": { + "INFO": { + "CLIENTID": "ID client" + }, + "REGENERATESECRET": "Regenerați secretul clientului", + "SELECTION": { + "TITLE": "API", + "DESCRIPTION": "API-uri în general" + }, + "AUTHMETHOD": { + "0": "De bază", + "1": "JWT cheie privată" + } + }, + "SAML": { + "SELECTION": { + "TITLE": "SAML", + "DESCRIPTION": "Aplicații SAML" + }, + "CONFIGSECTION": "Configurație SAML", + "CHOOSEMETADATASOURCE": "Furnizați configurația SAML utilizând una dintre următoarele opțiuni:", + "METADATAOPT1": "Opțiunea 1. Specificați adresa URL unde se află fișierul metadata", + "METADATAOPT2": "Opțiunea 2. Încărcați un fișier care conține XML-ul dvs. metadata", + "METADATAOPT3": "Opțiunea 3. Creați un fișier metadata minim din mers furnizând ENTITYID și ACS URL", + "UPLOAD": "Încărcați fișier XML", + "METADATA": "Metadata", + "METADATAFROMFILE": "Metadata din fișier", + "CERTIFICATE": "Certificat SAML", + "DOWNLOADCERT": "Descărcați certificatul SAML", + "CREATEMETADATA": "Creați metadata", + "ENTITYID": "ID entitate", + "ACSURL": "Adresă URL punct final ACS" + }, + "AUTHMETHODS": { + "CODE": { + "TITLE": "Cod", + "DESCRIPTION": "Schimbați codul de autorizare pentru tokenuri" + }, + "PKCE": { + "TITLE": "PKCE", + "DESCRIPTION": "Utilizați un hash aleatoriu în loc de un secret static al clientului pentru mai multă securitate" + }, + "POST": { + "TITLE": "POST", + "DESCRIPTION": "Trimiteți client_id și client_secret ca parte a formularului" + }, + "PK_JWT": { + "TITLE": "JWT cheie privată", + "DESCRIPTION": "Utilizați o cheie privată pentru a vă autoriza aplicația" + }, + "BASIC": { + "TITLE": "De bază", + "DESCRIPTION": "Autentificare cu nume de utilizator și parolă" + }, + "IMPLICIT": { + "TITLE": "Implicit", + "DESCRIPTION": "Obțineți token-urile direct de la punctul final de autorizare" + }, + "DEVICECODE": { + "TITLE": "Cod dispozitiv", + "DESCRIPTION": "Autorizați dispozitivul pe un computer sau smartphone." + }, + "CUSTOM": { + "TITLE": "Personalizat", + "DESCRIPTION": "Setarea dvs. nu corespunde cu nicio altă opțiune." + } + }, + "TOAST": { + "REACTIVATED": "Aplicația a fost reactivată.", + "DEACTIVATED": "Aplicația a fost dezactivată.", + "OIDCUPDATED": "Aplicația a fost actualizată.", + "APIUPDATED": "Aplicația a fost actualizată", + "UPDATED": "Aplicația a fost actualizată.", + "CREATED": "Aplicația a fost creată.", + "CLIENTSECRETREGENERATED": "secretul clientului a fost generat.", + "DELETED": "Aplicația a fost ștearsă.", + "CONFIGCHANGED": "Au fost detectate modificări!" + }, + "LOGINV2": { + "USEV2": "Utilizați noua UI de conectare", + "BASEURL": "Adresă URL de bază personalizată pentru noua UI de conectare" + } + }, + "GENDERS": { + "0": "Necunoscut", + "1": "Femeie", + "2": "Bărbat", + "3": "Altul" + }, + "LANGUAGES": { + "de": "Deutsch", + "en": "English", + "es": "Español", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", + "pl": "Polski", + "zh": "简体中文", + "bg": "Български", + "pt": "Portuguese", + "mk": "Македонски", + "cs": "Čeština", + "ru": "Русский", + "nl": "Nederlands", + "sv": "Svenska", + "id": "Bahasa Indonesia", + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" + }, + "MEMBER": { + "ADD": "Adăugați un manager", + "CREATIONTYPE": "Tip creare", + "CREATIONTYPES": { + "3": "IAM", + "2": "Organizație", + "0": "Proiect deținut", + "1": "Proiect acordat", + "4": "Proiect" + }, + "EDITROLE": "Editați rolurile", + "EDITFOR": "Editați rolurile pentru utilizator: {{value}}", + "DIALOG": { + "DELETE_TITLE": "Eliminați managerul", + "DELETE_DESCRIPTION": "Sunteți pe cale să eliminați un manager. Sigur doriți să continuați?" + }, + "SHOWDETAILS": "Faceți clic pentru a afișa detalii." + }, + "ROLESLABEL": "Roluri", + "GRANTS": { + "TITLE": "Autorizații", + "DESC": "Acestea sunt toate autorizațiile din organizația dvs.", + "DELETE": "Ștergeți autorizarea", + "EMPTY": "Nicio autorizare găsită", + "ADD": "Creați autorizarea", + "ADD_BTN": "Nou", + "PROJECT": { + "TITLE": "Autorizare", + "DESCRIPTION": "Definiți autorizații pentru proiectul specificat. Rețineți că puteți vedea numai intrările proiectelor și utilizatorilor pentru care aveți permisiunile." + }, + "USER": { + "TITLE": "Autorizare", + "DESCRIPTION": "Definiți autorizații pentru utilizatorul specificat. Rețineți că puteți vedea numai intrările proiectelor și utilizatorilor pentru care aveți permisiunile." + }, + "CREATE": { + "TITLE": "Creați autorizarea", + "DESCRIPTION": "Căutați organizația, proiectul și rolurile corespunzătoare." + }, + "EDIT": { + "TITLE": "Modificați autorizarea" + }, + "DETAIL": { + "TITLE": "Detaliile autorizării", + "DESCRIPTION": "Aici puteți vedea toate detaliile autorizării." + }, + "TOAST": { + "UPDATED": "Autorizarea a fost actualizată.", + "REMOVED": "Autorizarea a fost eliminată", + "BULKREMOVED": "Autorizațiile au fost eliminate.", + "CANTSHOWINFO": "Nu puteți vizita profilul acestui utilizator, deoarece nu sunteți membru al organizației căreia îi aparține acest utilizator" + }, + "DIALOG": { + "DELETE_TITLE": "Ștergeți autorizarea", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți o autorizare. Doriți să continuați?", + "BULK_DELETE_TITLE": "Ștergeți autorizațiile", + "BULK_DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți mai multe autorizații. Doriți să continuați?" + } + }, + "CHANGES": { + "LISTTITLE": "Ultimele modificări", + "BOTTOM": "Ați ajuns la sfârșitul listei.", + "LOADMORE": "Încărcați mai multe", + "ORG": { + "TITLE": "Activitate", + "DESCRIPTION": "Aici puteți vedea ultimele evenimente care au generat o modificare a organizației." + }, + "PROJECT": { + "TITLE": "Activitate", + "DESCRIPTION": "Aici puteți vedea ultimele evenimente care au generat o modificare a proiectului." + }, + "USER": { + "TITLE": "Activitate", + "DESCRIPTION": "Aici puteți vedea ultimele evenimente care au generat o modificare a utilizatorului." + } + } +} diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index ad9c17691f..c6ef31499e 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Потоки", "DESCRIPTION": "Выберите поток аутентификации и активируйте ваше действие на определенном событии в этом потоке." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, новая и улучшенная версия Actions, теперь доступна. Текущая версия всё ещё доступна, но дальнейшая разработка будет сосредоточена на новой версии, которая в конечном итоге заменит текущую." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,33 @@ "DESCRIPTION": "Срок действия неактивного токена обновления - это максимальное время, в течение которого токен обновления может оставаться неиспользованным." } }, + "WEB_KEYS": { + "DESCRIPTION": "Управляйте своими OIDC веб-ключами для безопасной подписи и валидации токенов в вашем экземпляре ZITADEL.", + "TABLE": { + "TITLE": "Активные и будущие веб-ключи", + "DESCRIPTION": "Ваши активные и будущие веб-ключи. Активация нового ключа приведёт к деактивации текущего.", + "NOTE": "Примечание: Конечная точка JWKs OIDC возвращает кэшируемый ответ (по умолчанию 5 минут). Избегайте слишком ранней активации ключа, так как он может ещё не быть доступен в кэше и у клиентов.", + "ACTIVATE": "Активировать следующий веб-ключ", + "ACTIVE": "В настоящее время активен", + "NEXT": "Следующий в очереди", + "FUTURE": "Будущий", + "WARNING": "Веб-ключу менее 5 минут" + }, + "CREATE": { + "TITLE": "Создать новый веб-ключ", + "DESCRIPTION": "Создание нового веб-ключа добавит его в ваш список. ZITADEL по умолчанию использует ключи RSA2048 с хешированием SHA256.", + "KEY_TYPE": "Тип ключа", + "BITS": "Биты", + "HASHER": "Алгоритм хеширования", + "CURVE": "Кривая" + }, + "PREVIOUS_TABLE": { + "TITLE": "Предыдущие веб-ключи", + "DESCRIPTION": "Это ваши предыдущие веб-ключи, которые больше не активны.", + "DEACTIVATED_ON": "Деактивирован" + } + }, + "MESSAGE_TEXTS": { "TITLE": "Тексты сообщений", "DESCRIPTION": "Настройте тексты ваших уведомлений по электронной почте или SMS. Если вы хотите отключить некоторые языки, ограничьте их в настройках языка ваших экземпляров.", @@ -501,6 +529,118 @@ "DOWNLOAD": "Скачать", "APPLY": "Применять" }, + "ACTIONSTWO": { + "BETA_NOTE": "Вы используете новую версию Actions V2, которая находится в бета-тестировании. Предыдущая версия 1 всё ещё доступна, но будет отключена в будущем. Пожалуйста, сообщайте о любых проблемах или отправляйте отзывы.", + "EXECUTION": { + "TITLE": "Действия", + "DESCRIPTION": "Действия позволяют запускать пользовательский код в ответ на API-запросы, события или определенные функции. Используйте их для расширения Zitadel, автоматизации рабочих процессов и интеграции с другими системами.", + "TYPES": { + "request": "Запрос", + "response": "Ответ", + "event": "События", + "function": "Функция" + }, + "DIALOG": { + "CREATE_TITLE": "Создать действие", + "UPDATE_TITLE": "Обновить действие", + "TYPE": { + "DESCRIPTION": "Выберите, когда вы хотите запустить это действие", + "REQUEST": { + "TITLE": "Запрос", + "DESCRIPTION": "Запросы, которые происходят внутри Zitadel. Это может быть что-то вроде вызова запроса на вход." + }, + "RESPONSE": { + "TITLE": "Ответ", + "DESCRIPTION": "Ответ на запрос внутри Zitadel. Подумайте об ответе, который вы получаете при получении пользователя." + }, + "EVENTS": { + "TITLE": "События", + "DESCRIPTION": "События, которые происходят внутри Zitadel. Это может быть что угодно, например, создание пользователем учетной записи, успешный вход и т. д." + }, + "FUNCTIONS": { + "TITLE": "Функции", + "DESCRIPTION": "Функции, которые вы можете вызвать внутри Zitadel. Это может быть что угодно, от отправки электронной почты до создания пользователя." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Выберите, применяется ли это действие ко всем запросам, к определенной службе (например, управление пользователями) или к одному запросу (например, создать пользователя).", + "ALL": { + "TITLE": "Все", + "DESCRIPTION": "Выберите это, если вы хотите запустить свое действие при каждом запросе" + }, + "ALL_EVENTS": "Выберите это, если хотите выполнять действие при каждом событии", + "SELECT_SERVICE": { + "TITLE": "Выбрать службу", + "DESCRIPTION": "Выберите службу Zitadel для вашего действия." + }, + "SELECT_METHOD": { + "TITLE": "Выбрать метод", + "DESCRIPTION": "Если вы хотите запустить только для определенного запроса, выберите его здесь", + "NOTE": "Если вы не выберете метод, ваше действие будет запускаться при каждом запросе в выбранной вами службе." + }, + "FUNCTIONNAME": { + "TITLE": "Имя функции", + "DESCRIPTION": "Выберите функцию, которую вы хотите запустить" + }, + "SELECT_GROUP": { + "TITLE": "Установить группу", + "DESCRIPTION": "Если вы хотите запустить только для группы событий, установите группу здесь" + }, + "SELECT_EVENT": { + "TITLE": "Выбрать событие", + "DESCRIPTION": "Если вы хотите запустить только для определенного события, укажите его здесь" + } + }, + "TARGET": { + "DESCRIPTION": "Вы можете выбрать запуск цели или запустить ее в тех же условиях, что и другие цели.", + "TARGET": { + "DESCRIPTION": "Цель, которую вы хотите запустить для этого действия" + }, + "CONDITIONS": { + "DESCRIPTION": "Условия выполнения" + } + } + }, + "TABLE": { + "CONDITION": "Условие", + "TYPE": "Тип", + "TARGET": "Цель", + "CREATIONDATE": "Дата создания" + } + }, + "TARGET": { + "TITLE": "Цели", + "DESCRIPTION": "Цель — это место назначения кода, который вы хотите запустить из действия. Создайте цель здесь и добавьте ее к своим действиям.", + "CREATE": { + "TITLE": "Создать свою цель", + "DESCRIPTION": "Создайте свою собственную цель за пределами Zitadel", + "NAME": "Имя", + "NAME_DESCRIPTION": "Дайте своей цели четкое, описательное имя, чтобы ее было легко идентифицировать позже", + "TYPE": "Тип", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Вызов", + "restAsync": "REST Асинхронный" + }, + "TYPES_DESCRIPTION": "Webhook, вызов обрабатывает код состояния, но ответ не имеет значения\nCall, вызов обрабатывает код состояния и ответ\nAsync, вызов не обрабатывает ни код состояния, ни ответ, но может выполняться параллельно с другими целями", + "ENDPOINT": "Конечная точка", + "ENDPOINT_DESCRIPTION": "Введите конечную точку, где размещен ваш код. Убедитесь, что он доступен для нас!", + "TIMEOUT": "Тайм-аут", + "TIMEOUT_DESCRIPTION": "Установите максимальное время, в течение которого ваша цель должна ответить. Если это займет больше времени, мы остановим запрос.", + "INTERRUPT_ON_ERROR": "Прервать при ошибке", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Остановите все выполнения, когда цели вернут ошибку", + "INTERRUPT_ON_ERROR_WARNING": "Внимание: опция «Прервать при ошибке» останавливает выполнение при сбое, что может привести к блокировке. Протестируйте с отключённой опцией, чтобы избежать блокировки входа/создания.", + "AWAIT_RESPONSE": "Ожидать ответа", + "AWAIT_RESPONSE_DESCRIPTION": "Мы подождем ответа, прежде чем делать что-либо еще. Полезно, если вы планируете использовать несколько целей для одного действия" + }, + "TABLE": { + "NAME": "Имя", + "ENDPOINT": "Конечная точка", + "CREATIONDATE": "Дата создания", + "REORDER": "Изменить порядок" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Имеет контроль над всем экземпляром, включая все организации", "IAM_OWNER_VIEWER": "Имеет разрешение на просмотр всего экземпляра, включая все организации", @@ -797,7 +937,10 @@ "PHONESECTION": "Номера телефонов", "PASSWORDSECTION": "Начальный пароль", "ADDRESSANDPHONESECTION": "Номер телефона", - "INITMAILDESCRIPTION": "Если выбраны оба варианта, электронное письмо для инициализации не будет отправлено. Если выбран только один из вариантов, будет отправлено письмо для предоставления/проверки данных." + "INITMAILDESCRIPTION": "Если выбраны оба варианта, электронное письмо для инициализации не будет отправлено. Если выбран только один из вариантов, будет отправлено письмо для предоставления/проверки данных.", + "SETUPAUTHENTICATIONLATER": "Настроить аутентификацию позже для этого пользователя.", + "INVITATION": "Отправить приглашение по электронной почте для настройки аутентификации и подтверждения электронной почты.", + "INITIALPASSWORD": "Установите начальный пароль для пользователя." }, "CODEDIALOG": { "TITLE": "Подтвердить номер телефона", @@ -1398,6 +1541,7 @@ "BRANDING": "Брендинг", "PRIVACYPOLICY": "Политика конфиденциальности", "OIDC": "Срок действия токена OIDC", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Отображение ключа", "SECURITY": "Настройки безопасности", "EVENTS": "События", @@ -1413,7 +1557,8 @@ "APPEARANCE": "Вид", "OTHER": "Другое", "STORAGE": "хранилище" - } + }, + "BETA": "БЕТА" }, "SETTING": { "LANGUAGES": { @@ -1443,7 +1588,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1542,6 +1688,16 @@ "ACTIONS_DESCRIPTION": "Actions v2 позволяют управлять выполнением данных и целевыми объектами. Если флаг включен, вы сможете использовать новый API и его функции.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Окончание сеанса", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Если флаг включен, вы сможете завершить отдельный сеанс из интерфейса пользователя входа, предоставив id_token с претензией `sid` в качестве id_token_hint на конечной точке end_session. Обратите внимание, что в настоящее время все сеансы одного и того же пользовательского агента (браузера) завершаются в интерфейсе пользователя входа. Сеансы, управляемые через API сеанса, уже позволяют завершать отдельные сеансы.", + "DEBUGOIDCPARENTERROR": "Отладка Ошибки Родителя OIDC", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Если флаг включен, ошибка родителя OIDC будет записана в консоль.", + "DISABLEUSERTOKENEVENT": "Отключить Событие Токена Пользователя", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Включить Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout реализует OpenID Connect Back-Channel Logout 1.0 и может использоваться для уведомления клиентов о завершении сеанса у поставщика OpenID.", + "PERMISSIONCHECKV2": "Проверка Разрешений V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Если флаг включен, вы сможете использовать новый API и его функции.", + "WEBKEY": "Веб-ключ", + "WEBKEY_DESCRIPTION": "Если флаг включен, вы сможете использовать новый API и его функции.", "STATES": { "INHERITED": "Наследовать", "ENABLED": "Включено", @@ -1554,7 +1710,10 @@ }, "RESET": "Установить все по умолчанию", "CONSOLEUSEV2USERAPI": "Используйте V2 API в консоли для создания пользователей", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "Когда этот флаг включен, консоль использует V2 User API для создания новых пользователей. С API V2 новые пользователи создаются без начального состояния." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Когда этот флаг включен, консоль использует V2 User API для создания новых пользователей. С API V2 новые пользователи создаются без начального состояния.", + "LOGINV2": "Вход V2", + "LOGINV2_DESCRIPTION": "Включение этой опции активирует новый интерфейс входа на основе TypeScript с улучшенной безопасностью, производительностью и возможностью настройки.", + "LOGINV2_BASEURI": "Базовый URI" }, "DIALOG": { "RESET": { @@ -1695,7 +1854,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "LOCALE": "Код языка", "LOCALES": { @@ -2696,7 +2857,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Добавить менеджера", diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index def5f45b2b..c356e635e0 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flöden", "DESCRIPTION": "Välj ett autentiseringsflöde och trigga din åtgärd vid en specifik händelse inom detta flöde." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, en ny och förbättrad version av Actions, är nu tillgänglig. Den nuvarande versionen är fortfarande tillgänglig, men framtida utveckling kommer att fokusera på den nya, som så småningom kommer att ersätta den nuvarande." }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "Den inaktiva förnyelsetokenens livslängd är den maximala tiden en förnyelsetoken kan vara oanvänd." } }, + "WEB_KEYS": { + "DESCRIPTION": "Hantera dina OIDC-webbnycklar för att säkert signera och validera tokens för din ZITADEL-instans.", + "TABLE": { + "TITLE": "Aktiva och framtida webbnycklar", + "DESCRIPTION": "Dina aktiva och kommande webbnycklar. Aktivering av en ny nyckel kommer att inaktivera den nuvarande.", + "NOTE": "Observera: JWKs OIDC-slutpunkten returnerar ett cachebart svar (standard 5 min). Undvik att aktivera en nyckel för tidigt, eftersom den kanske ännu inte är tillgänglig i cache och för klienter.", + "ACTIVATE": "Aktivera nästa webbnyckel", + "ACTIVE": "För närvarande aktiv", + "NEXT": "Nästa i kön", + "FUTURE": "Framtida", + "WARNING": "Webbnyckeln är mindre än 5 minuter gammal" + }, + "CREATE": { + "TITLE": "Skapa ny webbnyckel", + "DESCRIPTION": "Att skapa en ny webbnyckel lägger till den i din lista. ZITADEL använder som standard RSA2048-nycklar med en SHA256-hasher.", + "KEY_TYPE": "Nyckeltyp", + "BITS": "Bitar", + "HASHER": "Hasher", + "CURVE": "Kurva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Tidigare webbnycklar", + "DESCRIPTION": "Detta är dina tidigare webbnycklar som inte längre är aktiva.", + "DEACTIVATED_ON": "Inaktiverad den" + } + }, "MESSAGE_TEXTS": { "TITLE": "Meddelandetexter", "DESCRIPTION": "Anpassa texterna i dina notifikationsmail eller SMS-meddelanden. Om du vill inaktivera några av språken, begränsa dem i dina instansers språkinställningar.", @@ -502,6 +529,118 @@ "DOWNLOAD": "Ladda ner", "APPLY": "Tillämpa" }, + "ACTIONSTWO": { + "BETA_NOTE": "Du använder för närvarande nya Actions V2, som är i betaversion. Den tidigare versionen 1 är fortfarande tillgänglig men kommer att avvecklas i framtiden. Vänligen rapportera eventuella problem eller ge feedback.", + "EXECUTION": { + "TITLE": "Åtgärder", + "DESCRIPTION": "Åtgärder låter dig köra anpassad kod som svar på API-förfrågningar, händelser eller specifika funktioner. Använd dem för att utöka Zitadel, automatisera arbetsflöden och integrera med andra system.", + "TYPES": { + "request": "Förfrågan", + "response": "Svar", + "event": "Händelser", + "function": "Funktion" + }, + "DIALOG": { + "CREATE_TITLE": "Skapa en åtgärd", + "UPDATE_TITLE": "Uppdatera en åtgärd", + "TYPE": { + "DESCRIPTION": "Välj när du vill att denna åtgärd ska köras", + "REQUEST": { + "TITLE": "Förfrågan", + "DESCRIPTION": "Förfrågningar som sker inom Zitadel. Detta kan vara något som ett inloggningsförfrågningsanrop." + }, + "RESPONSE": { + "TITLE": "Svar", + "DESCRIPTION": "Ett svar från en förfrågan inom Zitadel. Tänk på svaret du får tillbaka från att hämta en användare." + }, + "EVENTS": { + "TITLE": "Händelser", + "DESCRIPTION": "Händelser som händer inom Zitadel. Detta kan vara vad som helst som en användare som skapar ett konto, en lyckad inloggning etc." + }, + "FUNCTIONS": { + "TITLE": "Funktioner", + "DESCRIPTION": "Funktioner som du kan anropa inom Zitadel. Detta kan vara allt från att skicka ett e-postmeddelande till att skapa en användare." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Välj om denna åtgärd gäller för alla förfrågningar, en specifik tjänst (t.ex. användarhantering) eller en enskild förfrågan (t.ex. skapa användare).", + "ALL": { + "TITLE": "Alla", + "DESCRIPTION": "Välj detta om du vill köra din åtgärd på varje förfrågan" + }, + "ALL_EVENTS": "Välj detta om du vill köra din åtgärd vid varje händelse", + "SELECT_SERVICE": { + "TITLE": "Välj tjänst", + "DESCRIPTION": "Välj en Zitadel-tjänst för din åtgärd." + }, + "SELECT_METHOD": { + "TITLE": "Välj metod", + "DESCRIPTION": "Om du bara vill köra på en specifik förfrågan, välj den här", + "NOTE": "Om du inte väljer en metod körs din åtgärd på varje förfrågan i din valda tjänst." + }, + "FUNCTIONNAME": { + "TITLE": "Funktionsnamn", + "DESCRIPTION": "Välj den funktion du vill köra" + }, + "SELECT_GROUP": { + "TITLE": "Ange grupp", + "DESCRIPTION": "Om du bara vill köra på en grupp händelser, ange gruppen här" + }, + "SELECT_EVENT": { + "TITLE": "Välj händelse", + "DESCRIPTION": "Om du bara vill köra på en specifik händelse, ange den här" + } + }, + "TARGET": { + "DESCRIPTION": "Du kan välja att köra ett mål eller att köra det under samma villkor som andra mål.", + "TARGET": { + "DESCRIPTION": "Målet du vill köra för denna åtgärd" + }, + "CONDITIONS": { + "DESCRIPTION": "Körningsvillkor" + } + } + }, + "TABLE": { + "CONDITION": "Villkor", + "TYPE": "Typ", + "TARGET": "Mål", + "CREATIONDATE": "Skapat datum" + } + }, + "TARGET": { + "TITLE": "Mål", + "DESCRIPTION": "Ett mål är destinationen för koden du vill köra från en åtgärd. Skapa ett mål här och lägg till det i dina åtgärder.", + "CREATE": { + "TITLE": "Skapa ditt mål", + "DESCRIPTION": "Skapa ditt eget mål utanför Zitadel", + "NAME": "Namn", + "NAME_DESCRIPTION": "Ge ditt mål ett tydligt, beskrivande namn för att göra det enkelt att identifiera senare", + "TYPE": "Typ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Anrop", + "restAsync": "REST Asynkron" + }, + "TYPES_DESCRIPTION": "Webhook, anropet hanterar statuskoden men svaret är irrelevant\nCall, anropet hanterar statuskoden och svaret\nAsync, anropet hanterar varken statuskod eller svar men kan anropas parallellt med andra mål", + "ENDPOINT": "Slutpunkt", + "ENDPOINT_DESCRIPTION": "Ange slutpunkten där din kod finns. Se till att den är tillgänglig för oss!", + "TIMEOUT": "Tidsgräns", + "TIMEOUT_DESCRIPTION": "Ange den maximala tid ditt mål har att svara. Om det tar längre tid stoppar vi förfrågan.", + "INTERRUPT_ON_ERROR": "Avbryt vid fel", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Stoppa alla körningar när målen returnerar ett fel", + "INTERRUPT_ON_ERROR_WARNING": "Varning: ”Avbryt vid fel” stoppar åtgärder vid fel och kan leda till att du blir utelåst. Testa med funktionen avstängd för att undvika att blockera inloggning/skapa.", + "AWAIT_RESPONSE": "Vänta på svar", + "AWAIT_RESPONSE_DESCRIPTION": "Vi väntar på ett svar innan vi gör något annat. Användbart om du avser att använda flera mål för en enda åtgärd" + }, + "TABLE": { + "NAME": "Namn", + "ENDPOINT": "Slutpunkt", + "CREATIONDATE": "Skapat datum", + "REORDER": "Ordna om" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Har kontroll över hela instansen, inklusive alla organisationer", "IAM_OWNER_VIEWER": "Har behörighet att granska hela instansen, inklusive alla organisationer", @@ -790,7 +929,10 @@ "PHONESECTION": "Telefonnummer", "PASSWORDSECTION": "Initialt lösenord", "ADDRESSANDPHONESECTION": "Telefonnummer", - "INITMAILDESCRIPTION": "Om båda alternativen är valda kommer inget e-postmeddelande för initialisering att skickas. Om endast ett av alternativen är valt kommer ett e-postmeddelande för att tillhandahålla/verifiera uppgifterna att skickas." + "INITMAILDESCRIPTION": "Om båda alternativen är valda kommer inget e-postmeddelande för initialisering att skickas. Om endast ett av alternativen är valt kommer ett e-postmeddelande för att tillhandahålla/verifiera uppgifterna att skickas.", + "SETUPAUTHENTICATIONLATER": "Ställ in autentisering senare för den här användaren.", + "INVITATION": "Skicka en inbjudningsmail för autentiseringsinställning och e-postverifiering.", + "INITIALPASSWORD": "Ställ in ett initialt lösenord för användaren." }, "CODEDIALOG": { "TITLE": "Verifiera telefonnummer", @@ -1358,6 +1500,7 @@ "BRANDING": "Varumärke", "PRIVACYPOLICY": "Externa länkar", "OIDC": "OIDC-token livstid och utgång", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Hemlighetsgenerator", "SECURITY": "Säkerhetsinställningar", "EVENTS": "Händelser", @@ -1373,7 +1516,8 @@ "APPEARANCE": "Utseende", "OTHER": "Övrigt", "STORAGE": "Lagring" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { @@ -1403,7 +1547,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1494,6 +1639,16 @@ "ACTIONS_DESCRIPTION": "Åtgärder v2 tillåter att hantera dataexekveringar och mål. Om flaggan är aktiverad kommer du att kunna använda det nya API:et och dess funktioner.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Session avslutning", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Om flaggan är aktiverad, kan du avsluta en enskild session från inloggningsgränssnittet genom att ange en id_token med ett `sid`-krav som id_token_hint på slutpunkten end_session. Observera att för närvarande alla sessioner från samma användaragent (webbläsare) avslutas i inloggningsgränssnittet. Sessioner som hanteras via Session API tillåter redan avslutning av enskilda sessioner.", + "DEBUGOIDCPARENTERROR": "Debugga OIDC Föräldrafel", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Om flaggan är aktiverad kommer OIDC-föräldrafel att loggas i konsolen.", + "DISABLEUSERTOKENEVENT": "Inaktivera Användartokenhändelse", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Aktivera Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout implementerar OpenID Connect Back-Channel Logout 1.0 och kan användas för att meddela klienter om sessionens avslutning hos OpenID-leverantören.", + "PERMISSIONCHECKV2": "Behörighetskontroll V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Om flaggan är aktiverad kan du använda den nya API:n och dess funktioner.", + "WEBKEY": "Webbnyckel", + "WEBKEY_DESCRIPTION": "Om flaggan är aktiverad kan du använda den nya API:n och dess funktioner.", "STATES": { "INHERITED": "Ärv", "ENABLED": "Aktiverad", @@ -1506,7 +1661,10 @@ }, "RESET": "Återställ allt till arv", "CONSOLEUSEV2USERAPI": "Använd V2 API i konsolen för att skapa användare", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "När denna flagga är aktiverad använder konsolen V2 User API för att skapa nya användare. Med V2 API startar nyligen skapade användare utan ett initialt tillstånd." + "CONSOLEUSEV2USERAPI_DESCRIPTION": "När denna flagga är aktiverad använder konsolen V2 User API för att skapa nya användare. Med V2 API startar nyligen skapade användare utan ett initialt tillstånd.", + "LOGINV2": "Inloggning V2", + "LOGINV2_DESCRIPTION": "Att aktivera detta startar det nya inloggningsgränssnittet baserat på TypeScript med förbättrad säkerhet, prestanda och anpassning.", + "LOGINV2_BASEURI": "Bas-URI" }, "DIALOG": { "RESET": { @@ -1643,7 +1801,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "E-postverifiering klar", @@ -2617,7 +2777,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Lägg till en administratör", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index af7e493459..8be3316b0b 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "流程", "DESCRIPTION": "选择一个认证流程,并在该流程中的特定事件上触发您的操作。" - } + }, + "ACTIONSTWO_NOTE": "Actions V2,一个全新改进版的Actions,现在已上线。目前版本仍可使用,但未来开发将专注于新版本,最终将取代当前版本。" }, "SETTINGS": { "INSTANCE": { @@ -185,6 +186,32 @@ "DESCRIPTION": "空闲刷新令牌的生命周期是刷新令牌可以未使用的最长时间。" } }, + "WEB_KEYS": { + "DESCRIPTION": "管理您的 OIDC Web 密钥,以安全地签署和验证您的 ZITADEL 实例的令牌。", + "TABLE": { + "TITLE": "活动和未来的 Web 密钥", + "DESCRIPTION": "您的当前活动和即将启用的 Web 密钥。激活新密钥将会停用当前密钥。", + "NOTE": "注意:JWKs OIDC 端点返回可缓存的响应(默认 5 分钟)。请避免过早激活密钥,否则它可能尚未在缓存或客户端中可用。", + "ACTIVATE": "激活下一个 Web 密钥", + "ACTIVE": "当前活动", + "NEXT": "队列中的下一个", + "FUTURE": "未来", + "WARNING": "Web密钥不到5分钟。" + }, + "CREATE": { + "TITLE": "创建新的 Web 密钥", + "DESCRIPTION": "创建新的 Web 密钥会将其添加到您的列表。ZITADEL 默认使用 RSA2048 密钥和 SHA256 哈希算法。", + "KEY_TYPE": "密钥类型", + "BITS": "位数", + "HASHER": "哈希算法", + "CURVE": "曲线" + }, + "PREVIOUS_TABLE": { + "TITLE": "先前的 Web 密钥", + "DESCRIPTION": "这些是您之前使用但不再活动的 Web 密钥。", + "DEACTIVATED_ON": "停用时间" + } + }, "MESSAGE_TEXTS": { "TITLE": "消息文本", "DESCRIPTION": "自定义您的通知电子邮件或短信消息的文本。如果您想禁用某些语言,请在您的实例语言设置中限制它们。", @@ -502,6 +529,118 @@ "DOWNLOAD": "下载", "APPLY": "申请" }, + "ACTIONSTWO": { + "BETA_NOTE": "您目前正在使用新的 Actions V2(测试版)。之前的版本1仍可使用,但未来将停止支持。请报告任何问题或反馈意见。", + "EXECUTION": { + "TITLE": "操作", + "DESCRIPTION": "操作允许您运行自定义代码以响应 API 请求、事件或特定函数。使用它们来扩展 Zitadel、自动化工作流程并与其他系统集成。", + "TYPES": { + "request": "请求", + "response": "响应", + "event": "事件", + "function": "函数" + }, + "DIALOG": { + "CREATE_TITLE": "创建操作", + "UPDATE_TITLE": "更新操作", + "TYPE": { + "DESCRIPTION": "选择您希望此操作运行的时间", + "REQUEST": { + "TITLE": "请求", + "DESCRIPTION": "Zitadel 中发生的请求。这可能是登录请求调用之类的操作。" + }, + "RESPONSE": { + "TITLE": "响应", + "DESCRIPTION": "来自 Zitadel 中请求的响应。考虑一下从获取用户返回的响应。" + }, + "EVENTS": { + "TITLE": "事件", + "DESCRIPTION": "Zitadel 中发生的事件。这可能是用户创建帐户、成功登录等任何操作。" + }, + "FUNCTIONS": { + "TITLE": "函数", + "DESCRIPTION": "您可以在 Zitadel 中调用的函数。这可能是从发送电子邮件到创建用户的任何操作。" + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "选择此操作是适用于所有请求、特定服务(例如用户管理)还是单个请求(例如创建用户)。", + "ALL": { + "TITLE": "全部", + "DESCRIPTION": "如果您希望在每个请求上运行您的操作,请选择此项" + }, + "ALL_EVENTS": "如果您想在每个事件上运行操作,请选择此项", + "SELECT_SERVICE": { + "TITLE": "选择服务", + "DESCRIPTION": "为您的操作选择一个 Zitadel 服务。" + }, + "SELECT_METHOD": { + "TITLE": "选择方法", + "DESCRIPTION": "如果您只想在特定请求上执行,请在此处选择它", + "NOTE": "如果您不选择方法,您的操作将在您选择的服务中的每个请求上运行。" + }, + "FUNCTIONNAME": { + "TITLE": "函数名称", + "DESCRIPTION": "选择您要执行的函数" + }, + "SELECT_GROUP": { + "TITLE": "设置组", + "DESCRIPTION": "如果您只想在事件组上执行,请在此处设置组" + }, + "SELECT_EVENT": { + "TITLE": "选择事件", + "DESCRIPTION": "如果您只想在特定事件上执行,请在此处指定它" + } + }, + "TARGET": { + "DESCRIPTION": "您可以选择执行目标,或在与其他目标相同的条件下运行它。", + "TARGET": { + "DESCRIPTION": "您要为此操作执行的目标" + }, + "CONDITIONS": { + "DESCRIPTION": "执行条件" + } + } + }, + "TABLE": { + "CONDITION": "条件", + "TYPE": "类型", + "TARGET": "目标", + "CREATIONDATE": "创建日期" + } + }, + "TARGET": { + "TITLE": "目标", + "DESCRIPTION": "目标是您要从操作中执行的代码的目标。在此处创建一个目标并将其添加到您的操作中。", + "CREATE": { + "TITLE": "创建您的目标", + "DESCRIPTION": "在 Zitadel 外部创建您自己的目标", + "NAME": "名称", + "NAME_DESCRIPTION": "为您的目标提供清晰、描述性的名称,以便稍后轻松识别", + "TYPE": "类型", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST 调用", + "restAsync": "REST 异步" + }, + "TYPES_DESCRIPTION": "Webhook,调用处理状态码但响应无关紧要\nCall,调用处理状态码和响应\nAsync,调用既不处理状态码也不处理响应,但可以与其他目标并行调用", + "ENDPOINT": "端点", + "ENDPOINT_DESCRIPTION": "输入您的代码托管的端点。确保我们可以访问它!", + "TIMEOUT": "超时", + "TIMEOUT_DESCRIPTION": "设置您的目标必须响应的最大时间。如果花费的时间更长,我们将停止请求。", + "INTERRUPT_ON_ERROR": "错误时中断", + "INTERRUPT_ON_ERROR_DESCRIPTION": "当目标返回错误时,停止所有执行", + "INTERRUPT_ON_ERROR_WARNING": "注意:“出错时中断”会在失败时停止操作,存在被锁定的风险。请在禁用该选项的情况下进行测试,以避免阻止登录/创建。", + "AWAIT_RESPONSE": "等待响应", + "AWAIT_RESPONSE_DESCRIPTION": "我们将在执行任何其他操作之前等待响应。如果您打算为单个操作使用多个目标,这将非常有用" + }, + "TABLE": { + "NAME": "名称", + "ENDPOINT": "端点", + "CREATIONDATE": "创建日期", + "REORDER": "重新排序" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "控制整个实例,包括所有组织", "IAM_OWNER_VIEWER": "有权审查整个实例,包括所有组织", @@ -790,7 +929,10 @@ "PHONESECTION": "手机号码", "PASSWORDSECTION": "初始密码", "ADDRESSANDPHONESECTION": "手机号码", - "INITMAILDESCRIPTION": "如果选择了这两个选项,则不会发送初始化电子邮件。如果只选择了其中一个选项,将发送一封提供/验证数据的邮件。" + "INITMAILDESCRIPTION": "如果选择了这两个选项,则不会发送初始化电子邮件。如果只选择了其中一个选项,将发送一封提供/验证数据的邮件。", + "SETUPAUTHENTICATIONLATER": "稍后为此用户设置身份验证。", + "INVITATION": "发送邀请邮件以进行身份验证设置和电子邮件验证。", + "INITIALPASSWORD": "为用户设置初始密码。" }, "CODEDIALOG": { "TITLE": "验证手机号码", @@ -1354,6 +1496,7 @@ "BRANDING": "品牌标识", "PRIVACYPOLICY": "隐私政策", "OIDC": "OIDC 令牌有效期和过期时间", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "验证码外观", "SECURITY": "安全设置", "EVENTS": "活动", @@ -1369,7 +1512,8 @@ "APPEARANCE": "外观", "OTHER": "其他", "STORAGE": "贮存" - } + }, + "BETA": "测试版" }, "SETTING": { "LANGUAGES": { @@ -1399,7 +1543,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1490,6 +1635,16 @@ "ACTIONS_DESCRIPTION": "Actions v2 可以管理数据执行和目标。如果启用此标志,您将可以使用新的 API 及其功能。", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 终止会话", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "如果启用了标志,您可以通过在 end_session 端点上提供带有 `sid` 声明的 id_token 作为 id_token_hint 来从登录 UI 终止单个会话。 请注意,目前所有来自同一用户代理(浏览器)的会话都在登录 UI 中终止。 通过会话 API 管理的会话已经允许终止单个会话。", + "DEBUGOIDCPARENTERROR": "调试 OIDC 父错误", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "如果启用该标志,OIDC 父错误将记录在控制台中。", + "DISABLEUSERTOKENEVENT": "禁用用户令牌事件", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "启用 Backchannel 注销", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel 注销实现了 OpenID Connect Back-Channel Logout 1.0,可用于通知客户端在 OpenID 提供商处终止会话。", + "PERMISSIONCHECKV2": "权限检查 V2", + "PERMISSIONCHECKV2_DESCRIPTION": "如果启用该标志,您将能够使用新的 API 及其功能。", + "WEBKEY": "Web 密钥", + "WEBKEY_DESCRIPTION": "如果启用该标志,您将能够使用新的 API 及其功能。", "STATES": { "INHERITED": "继承", "ENABLED": "已启用", @@ -1502,7 +1657,10 @@ }, "RESET": "全部设置为继承", "CONSOLEUSEV2USERAPI": "在控制台中使用V2 API创建用户。", - "CONSOLEUSEV2USERAPI_DESCRIPTION": "启用此标志时,控制台使用V2用户API创建新用户。使用V2 API,新创建的用户将以无初始状态开始。" + "CONSOLEUSEV2USERAPI_DESCRIPTION": "启用此标志时,控制台使用V2用户API创建新用户。使用V2 API,新创建的用户将以无初始状态开始。", + "LOGINV2": "登录 V2", + "LOGINV2_DESCRIPTION": "启用此选项将激活基于 TypeScript 的新登录界面,具有更高的安全性、性能和可定制性。", + "LOGINV2_BASEURI": "基础 URI" }, "DIALOG": { "RESET": { @@ -1638,7 +1796,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "电子邮件验证完成", @@ -2588,7 +2748,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "添加管理者", diff --git a/console/src/styles/table.scss b/console/src/styles/table.scss index 933366607e..3b177e07e7 100644 --- a/console/src/styles/table.scss +++ b/console/src/styles/table.scss @@ -133,6 +133,11 @@ color: if($is-dark-theme, #ffc1c1, #620e0e); background-color: if($is-dark-theme, map-get($background, state-inactive), #ffc1c1); } + + &.neutral { + background: if($is-dark-theme, #01489c78, #47a8ff82); + color: if($is-dark-theme, #47a8ff, #01489c); + } } .bg-state { diff --git a/console/yarn.lock b/console/yarn.lock index d91242c992..2e586abedb 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -3452,22 +3452,22 @@ js-yaml "^3.10.0" tslib "^2.4.0" -"@zitadel/client@^1.0.7": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.0.7.tgz#39dc8d3d10bfa01e5cf56205ba188f79c39f052d" - integrity sha512-sZG4NEa8vQBt3+4W1AesY+5DstDBuZiqGH2EM+UqbO5D93dlDZInXqZ5oRE7RSl2Bk5ED9mbMFrB7b8DuRw72A== +"@zitadel/client@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.2.0.tgz#8cdc3090f75fcf3a78c4f0266d3c56a0cca6821a" + integrity sha512-Q20nXhKD7VDb8D1UxhDxubC70GFrSPckrJviPR/rAfRR5slUIRTk3AvDS6Q1WvUn4Xtt+btnq52Z5O8lZtVG0w== dependencies: "@bufbuild/protobuf" "^2.2.2" "@connectrpc/connect" "^2.0.0" "@connectrpc/connect-node" "^2.0.0" "@connectrpc/connect-web" "^2.0.0" - "@zitadel/proto" "1.0.4" + "@zitadel/proto" "1.2.0" jose "^5.3.0" -"@zitadel/proto@1.0.4", "@zitadel/proto@^1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.4.tgz#e2fe9895f2960643c3619191255aa2f4913ad873" - integrity sha512-s13ZMhuOTe0b+geV+JgJud+kpYdq7TgkuCe7RIY+q4Xs5KC0FHMKfvbAk/jpFbD+TSQHiwo/TBNZlGHdwUR9Ig== +"@zitadel/proto@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.2.0.tgz#9b9a40defcd9e8464627cc99ac3fd7bcf8994ffd" + integrity sha512-OqHgyCnD9l950xswdVNPIsLA01qSpOPf+0bYqYJWHafytIBbvGNJRnypu4X0LnaFXLM6LakkP4pWYeiGLmwxaw== dependencies: "@bufbuild/protobuf" "^2.2.2" diff --git a/deploy/knative/cockroachdb-statefulset-single-node.yaml b/deploy/knative/cockroachdb-statefulset-single-node.yaml deleted file mode 100644 index cf22db6f44..0000000000 --- a/deploy/knative/cockroachdb-statefulset-single-node.yaml +++ /dev/null @@ -1,169 +0,0 @@ -# Generated file, DO NOT EDIT. Source: cloud/kubernetes/templates/cockroachdb-statefulset.yaml -apiVersion: v1 -kind: Service -metadata: - # This service is meant to be used by clients of the database. It exposes a ClusterIP that will - # automatically load balance connections to the different database pods. - name: cockroachdb-public - labels: - app: cockroachdb -spec: - ports: - # The main port, served by gRPC, serves Postgres-flavor SQL, internode - # traffic and the cli. - - port: 26257 - targetPort: 26257 - name: grpc - # The secondary port serves the UI as well as health and debug endpoints. - - port: 8080 - targetPort: 8080 - name: http - selector: - app: cockroachdb ---- -apiVersion: v1 -kind: Service -metadata: - # This service only exists to create DNS entries for each pod in the stateful - # set such that they can resolve each other's IP addresses. It does not - # create a load-balanced ClusterIP and should not be used directly by clients - # in most circumstances. - name: cockroachdb - labels: - app: cockroachdb - annotations: - # Use this annotation in addition to the actual publishNotReadyAddresses - # field below because the annotation will stop being respected soon but the - # field is broken in some versions of Kubernetes: - # https://github.com/kubernetes/kubernetes/issues/58662 - service.alpha.kubernetes.io/tolerate-unready-endpoints: "true" - # Enable automatic monitoring of all instances when Prometheus is running in the cluster. - prometheus.io/scrape: "true" - prometheus.io/path: "_status/vars" - prometheus.io/port: "8080" -spec: - ports: - - port: 26257 - targetPort: 26257 - name: grpc - - port: 8080 - targetPort: 8080 - name: http - # We want all pods in the StatefulSet to have their addresses published for - # the sake of the other CockroachDB pods even before they're ready, since they - # have to be able to talk to each other in order to become ready. - publishNotReadyAddresses: true - clusterIP: None - selector: - app: cockroachdb ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: cockroachdb -spec: - serviceName: "cockroachdb" - replicas: 1 - selector: - matchLabels: - app: cockroachdb - template: - metadata: - labels: - app: cockroachdb - spec: - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app - operator: In - values: - - cockroachdb - topologyKey: kubernetes.io/hostname - containers: - - name: cockroachdb - image: cockroachdb/cockroach:latest-v24.3 - imagePullPolicy: IfNotPresent - # TODO: Change these to appropriate values for the hardware that you're running. You can see - # the resources that can be allocated on each of your Kubernetes nodes by running: - # kubectl describe nodes - # Note that requests and limits should have identical values. - resources: - requests: - cpu: "1" - memory: "4Gi" - limits: - cpu: "1" - memory: "4Gi" - ports: - - containerPort: 26257 - name: grpc - - containerPort: 8080 - name: http -# We recommend that you do not configure a liveness probe on a production environment, as this can impact the availability of production databases. -# livenessProbe: -# httpGet: -# path: "/health" -# port: http -# initialDelaySeconds: 30 -# periodSeconds: 5 - readinessProbe: - httpGet: - path: "/health?ready=1" - port: http - initialDelaySeconds: 10 - periodSeconds: 5 - failureThreshold: 2 - volumeMounts: - - name: datadir - mountPath: /cockroach/cockroach-data - env: - - name: COCKROACH_CHANNEL - value: kubernetes-insecure - - name: GOMAXPROCS - valueFrom: - resourceFieldRef: - resource: limits.cpu - divisor: "1" - - name: MEMORY_LIMIT_MIB - valueFrom: - resourceFieldRef: - resource: limits.memory - divisor: "1Mi" - command: - - "/bin/bash" - - "-ecx" - # The use of qualified `hostname -f` is crucial: - # Other nodes aren't able to look up the unqualified hostname. - - exec - /cockroach/cockroach - start-single-node - --logtostderr - --insecure - --advertise-host $(hostname -f) - --http-addr 0.0.0.0 - --cache $(expr $MEMORY_LIMIT_MIB / 4)MiB - --max-sql-memory $(expr $MEMORY_LIMIT_MIB / 4)MiB - # No pre-stop hook is required, a SIGTERM plus some time is all that's - # needed for graceful shutdown of a node. - terminationGracePeriodSeconds: 60 - volumes: - - name: datadir - persistentVolumeClaim: - claimName: datadir - podManagementPolicy: Parallel - updateStrategy: - type: RollingUpdate - volumeClaimTemplates: - - metadata: - name: datadir - spec: - accessModes: - - "ReadWriteOnce" - resources: - requests: - storage: 100Gi diff --git a/deploy/knative/zitadel-knative-service.yaml b/deploy/knative/zitadel-knative-service.yaml deleted file mode 100644 index 5271f99253..0000000000 --- a/deploy/knative/zitadel-knative-service.yaml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: serving.knative.dev/v1 -kind: Service -metadata: - creationTimestamp: null - name: zitadel -spec: - template: - metadata: - annotations: - client.knative.dev/user-image: ghcr.io/zitadel/zitadel:latest - creationTimestamp: null - spec: - containerConcurrency: 0 - containers: - - args: - - admin - - start-from-init - - --masterkey - - MasterkeyNeedsToHave32Characters - env: - - name: ZITADEL_DATABASE_COCKROACH_HOST - value: cockroachdb - - name: ZITADEL_EXTERNALSECURE - value: "false" - - name: ZITADEL_TLS_ENABLED - value: "false" - - name: ZITADEL_EXTERNALPORT - value: "80" - - name: ZITADEL_EXTERNALDOMAIN - value: zitadel.default.127.0.0.1.sslip.io - image: ghcr.io/zitadel/zitadel:latest - name: user-container - ports: - - containerPort: 8080 - protocol: TCP - readinessProbe: - successThreshold: 1 - tcpSocket: - port: 0 - resources: {} - enableServiceLinks: false - timeoutSeconds: 300 diff --git a/docs/docs/apis/actions/external-authentication.md b/docs/docs/apis/actions/external-authentication.md index 6a3a4d5551..114185871b 100644 --- a/docs/docs/apis/actions/external-authentication.md +++ b/docs/docs/apis/actions/external-authentication.md @@ -18,6 +18,8 @@ The trigger is represented by the following Ids in the API: `TRIGGER_TYPE_POST_A The first parameter contains the following fields - `accessToken` *string* The access token returned by the identity provider. This can be an opaque token or a JWT + - `refreshToken` *string* + The refresh token returned by the identity provider if there is one. This is most likely to be an opaque token. - `claimsJSON()` [*idTokenClaims*](../openidoauth/claims) Returns all claims of the id token - `getClaim(key)` *Any* diff --git a/docs/docs/apis/actions/v3/usage.md b/docs/docs/apis/actions/v3/usage.md deleted file mode 100644 index 2e89f3ce36..0000000000 --- a/docs/docs/apis/actions/v3/usage.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -title: Using Actions ---- - -The Action API provides a flexible mechanism for customizing and extending the functionality of ZITADEL. By allowing you to define targets and executions, you can implement custom workflows triggered on an API requests and responses, events or specific functions. - -**How it works:** -- Create Target -- Set Execution with condition and target -- Custom Code will be triggered and executed - -**Use Cases:** -- User Management: Automate provisioning user data to external systems when users are crreated, updated or deleted. -- Security: Implement IP blocking or rate limiting based on API usage patterns. -- Extend Workflows: Automatically setup resources in your application, when a new organization in ZITADEL is created. -- Token extension: Add custom claims to the tokens. - -## Endpoints - -ZITADEL sends an HTTP Post request to the endpoint set as Target, the received request than can be edited and send back or custom processes can be handled. - -### Sent information Request - -The information sent to the Endpoint is structured as JSON: - -```json -{ - "fullMethod": "full method of the GRPC call", - "instanceID": "instanceID of the called instance", - "orgID": "ID of the organization related to the calling context", - "projectID": "ID of the project related to the used application", - "userID": "ID of the calling user", - "request": "full request of the call" -} -``` - -### Sent information Response - -The information sent to the Endpoint is structured as JSON: - -```json -{ - "fullMethod": "full method of the GRPC call", - "instanceID": "instanceID of the called instance", - "orgID": "ID of the organization related to the calling context", - "projectID": "ID of the project related to the used application", - "userID": "ID of the calling user", - "request": "full request of the call", - "response": "full response of the call" -} -``` - -## Target - -The Target describes how ZITADEL interacts with the Endpoint. - -There are different types of Targets: - -- `Webhook`, the call handles the status code but response is irrelevant, can be InterruptOnError -- `Call`, the call handles the status code and response, can be InterruptOnError -- `Async`, the call handles neither status code nor response, but can be called in parallel with other Targets - -`InterruptOnError` means that the Execution gets interrupted if any of the calls return with a status code >= 400, and the next Target will not be called anymore. - -The API documentation to create a target can be found [here](/apis/resources/action_service_v3/zitadel-actions-create-target) - -### Content Signing - -To ensure the integrity of request content, each call includes a 'ZITADEL-Signature' in the headers. This header contains an HMAC value computed from the request content and a timestamp, which can be used to time out requests. The logic for this process is provided in 'pkg/actions/signing.go'. The goal is to verify that the HMAC value in the header matches the HMAC value computed by the Target, ensuring that the sent and received requests are identical. - -Each Target resource now contains also a Signing Key, which gets generated and returned when a Target is [created](/apis/resources/action_service_v3/zitadel-actions-create-target), -and can also be newly generated when a Target is [patched](/apis/resources/action_service_v3/zitadel-actions-patch-target). - -## Execution - -ZITADEL decides on specific conditions if one or more Targets have to be called. -The Execution resource contains 2 parts, the condition and the called targets. - -The condition can be defined for 4 types of processes: - -- `Requests`, before a request is processed by ZITADEL -- `Responses`, before a response is sent back to the application -- `Functions`, handling specific functionality in the logic of ZITADEL -- `Events`, after a specific event happened and was stored in ZITADEL - -The API documentation to set an Execution can be found [here](/apis/resources/action_service_v3/zitadel-actions-set-execution) - -### Condition Best Match - -As the conditions can be defined on different levels, ZITADEL tries to find out which Execution is the best match. -This means that for example if you have an Execution defined on `all requests`, on the service `zitadel.user.v2.UserService` and on `/zitadel.user.v2.UserService/AddHumanUser`, -ZITADEL would with a call on the `/zitadel.user.v2.UserService/AddHumanUser` use the Executions with the following priority: - -1. `/zitadel.user.v2.UserService/AddHumanUser` -2. `zitadel.user.v2.UserService` -3. `all` - -If you then have a call on `/zitadel.user.v2.UserService/UpdateHumanUser` the following priority would be found: - -1. `zitadel.user.v2.UserService` -2. `all` - -And if you use a different service, for example `zitadel.session.v2.SessionService`, then the `all` Execution would still be used. - -### Targets and Includes - -:::info -Includes are limited to 3 levels, which mean that include1->include2->include3 is the maximum for now. -If you have feedback to the include logic, or a reason why 3 levels are not enough, please open [an issue on github](https://github.com/zitadel/zitadel/issues) or [start a discussion on github](https://github.com/zitadel/zitadel/discussions)/[start a topic on discord](https://zitadel.com/chat) -::: - -An execution can not only contain a list of Targets, but also Includes. -The Includes can be defined in the Execution directly, which means you include all defined Targets by a before set Execution. - -If you define 2 Executions as follows: - -```json -{ - "condition": { - "request": { - "service": "zitadel.user.v2.UserService" - } - }, - "targets": [ - { - "target": "" - } - ] -} -``` - -```json -{ - "condition": { - "request": { - "method": "/zitadel.user.v2.UserService/AddHumanUser" - } - }, - "targets": [ - { - "target": "" - }, - { - "include": { - "request": { - "service": "zitadel.user.v2.UserService" - } - } - } - ] -} -``` - -The called Targets on "/zitadel.user.v2.UserService/AddHumanUser" would be, in order: - -1. `` -2. `` - -### Condition for Requests and Responses - -For Request and Response there are 3 levels the condition can be defined: - -- `Method`, handling a request or response of a specific GRPC full method, which includes the service name and method of the ZITADEL API -- `Service`, handling any request or response under a service of the ZITADEL API -- `All`, handling any request or response under the ZITADEL API - -The available conditions can be found under: -- [All available Methods](/apis/resources/action_service_v3/zitadel-actions-list-execution-methods), for example `/zitadel.user.v2.UserService/AddHumanUser` -- [All available Services](/apis/resources/action_service_v3/zitadel-actions-list-execution-services), for example `zitadel.user.v2.UserService` - -### Condition for Functions - -Replace the current Actions with the following flows: - -- [Internal Authentication](/apis/actions/internal-authentication) -- [External Authentication](/apis/actions/external-authentication) -- [Complement Token](/apis/actions/complement-token) -- [Customize SAML Response](/apis/actions/customize-samlresponse) - -The available conditions can be found under [all available Functions](/apis/resources/action_service_v3/zitadel-actions-list-execution-functions). - -### Condition for Events - -For event there are 3 levels the condition can be defined: - -- Event, handling a specific event -- Group, handling a specific group of events -- All, handling any event in ZITADEL - -The concept of events can be found under [Events](/concepts/architecture/software#events) - -### Error forwarding - -If you want to forward a specific error from the Target through ZITADEL, you can provide a response from the Target with status code 200 and a JSON in the following format: - -```json -{ - "forwardedStatusCode": 403, - "forwardedErrorMessage": "Call is forbidden through the IP AllowList definition" -} -``` - -Only values from 400 to 499 will be forwarded through ZITADEL, other StatusCodes will end in a PreconditionFailed error. - -If the Target returns any other status code than >= 200 and < 299, the execution is looked at as failed, and a PreconditionFailed error is logged. diff --git a/docs/docs/apis/benchmarks/_template.mdx b/docs/docs/apis/benchmarks/_template.mdx index 4bbb9e0b74..f015d20768 100644 --- a/docs/docs/apis/benchmarks/_template.mdx +++ b/docs/docs/apis/benchmarks/_template.mdx @@ -48,7 +48,7 @@ TODO: describe the outcome of the test? | ZITADEL Version | | | ZITADEL Configuration | | | ZITADEL feature flags | | -| Database | type: crdb / psql
version: | +| Database | type: psql
version: | | Database location | | | Database specification | vCPU:
memory: Gb | | ZITADEL metrics during test | | diff --git a/docs/docs/apis/benchmarks/index.mdx b/docs/docs/apis/benchmarks/index.mdx index e5d89dbae8..a0979f0081 100644 --- a/docs/docs/apis/benchmarks/index.mdx +++ b/docs/docs/apis/benchmarks/index.mdx @@ -57,9 +57,9 @@ The following metrics must be collected for each test iteration. The metrics are | ZITADEL Version | Setup | The version of zitadel deployed | Semantic version or commit | | ZITADEL Configuration | Setup | Configuration of zitadel which deviates from the defaults and is not secret | yaml | | ZITADEL feature flags | Setup | Changed feature flags | yaml | -| Database | Setup | Database type and version | **type**: crdb / psql **version**: semantic version | +| Database | Setup | Database type and version | **type**: psql **version**: semantic version | | Database location | Setup | Region or location of the deployment of the database. If not further specified the hoster is Google Cloud SQL | Location / Region | -| Database specification | Setup | The description must at least clarify the following metrics: vCPU, Memory and egress bandwidth (Scale) | **vCPU**: Amount of threads ([additional info](https://cloud.google.com/compute/docs/cpu-platforms)) **memory**: GB **egress bandwidth**:Gbps **scale**: Amount of crdb nodes if crdb is used | +| Database specification | Setup | The description must at least clarify the following metrics: vCPU, Memory and egress bandwidth (Scale) | **vCPU**: Amount of threads ([additional info](https://cloud.google.com/compute/docs/cpu-platforms)) **memory**: GB **egress bandwidth**:Gbps | | ZITADEL metrics during test | Result | This metric helps understanding the bottlenecks of the executed test. At least the following metrics must be provided: CPU usage Memory usage | **CPU usage** in percent **Memory usage** in percent | | Observed errors | Result | Errors worth mentioning, mostly unexpected errors | description | | Top 3 most expensive database queries | Result | The execution plan of the top 3 most expensive database queries during the test execution | database execution plan | diff --git a/docs/docs/concepts/architecture/secrets.md b/docs/docs/concepts/architecture/secrets.md index 4156f77a26..f8f195114b 100644 --- a/docs/docs/concepts/architecture/secrets.md +++ b/docs/docs/concepts/architecture/secrets.md @@ -70,6 +70,7 @@ The following hash algorithms are supported: - bcrypt (Default) - md5: implementation of md5Crypt with salt and password shuffling [^2] - md5plain: md5 digest of a password without salt [^2] +- md5salted: md5 digest of a salted password [^2] - scrypt - pbkdf2 diff --git a/docs/docs/concepts/architecture/software.md b/docs/docs/concepts/architecture/software.md index 07265b6de5..dc6f2b56c7 100644 --- a/docs/docs/concepts/architecture/software.md +++ b/docs/docs/concepts/architecture/software.md @@ -1,51 +1,51 @@ --- -title: ZITADEL's Software Architecture +title: Zitadel's Software Architecture sidebar_label: Software Architecture --- -ZITADEL is built with two essential patterns. Event Sourcing (ES) and Command and Query Responsibility Segregation (CQRS). -Due to the nature of Event Sourcing ZITADEL provides the unique capability to generate a strong audit trail of ALL the things that happen to its resources, without compromising on storage cost or audit trail length. +Zitadel is built with two essential patterns. Event Sourcing (ES) and Command and Query Responsibility Segregation (CQRS). +Due to the nature of Event Sourcing Zitadel provides the unique capability to generate a strong audit trail of ALL the things that happen to its resources, without compromising on storage cost or audit trail length. -The combination of ES and CQRS makes ZITADEL eventual consistent which, from our perspective, is a great benefit in many ways. +The combination of ES and CQRS makes Zitadel eventual consistent which, from our perspective, is a great benefit in many ways. It allows us to build a Source of Records (SOR) which is the one single point of truth for all computed states. The SOR needs to be transaction safe to make sure all operations are in order. You can read more about this in our [ES documentation](../eventstore/overview). -Each ZITADEL binary contains all components necessary to serve traffic +Each Zitadel binary contains all components necessary to serve traffic From serving the API, rendering GUI's, background processing of events and task. -This All in One (AiO) approach makes operating ZITADEL simple. +This All in One (AiO) approach makes operating Zitadel simple. ## The Architecture -ZITADELs software architecture is built around multiple components at different levels. +Zitadels software architecture is built around multiple components at different levels. This chapter should give you an idea of the components as well as the different layers. ![Software Architecture](/img/zitadel_software_architecture.png) ### Service Layer -The service layer includes all components who are potentially exposed to consumers of ZITADEL. +The service layer includes all components who are potentially exposed to consumers of Zitadel. #### HTTP Server The http server is responsible for the following functions: -- serving the management GUI called ZITADEL Console +- serving the management GUI called Zitadel Console - serving the static assets - rendering server side html (login, password-reset, verification, ...) #### API Server -The API layer consist of the multiple APIs provided by ZITADEL. Each serves a dedicated purpose. -All APIs of ZITADEL are always available as gRCP, gRPC-web and REST service. +The API layer consist of the multiple APIs provided by Zitadel. Each serves a dedicated purpose. +All APIs of Zitadel are always available as gRCP, gRPC-web and REST service. The only exception is the [OpenID Connect & OAuth](/apis/openidoauth/endpoints) and [Asset API](/apis/introduction#assets) due their unique nature. -- [OpenID Connect & OAuth](/apis/openidoauth/endpoints) - allows to request authentication and authorization of ZITADEL -- [SAML](/apis/saml/endpoints) - allows to request authentication and authorization of ZITADEL through the SAML standard +- [OpenID Connect & OAuth](/apis/openidoauth/endpoints) - allows to request authentication and authorization of Zitadel +- [SAML](/apis/saml/endpoints) - allows to request authentication and authorization of Zitadel through the SAML standard - [Authentication API](/apis/introduction#authentication) - allow a user to do operation in its own context -- [Management API](/apis/introduction#management) - allows an admin or machine to manage the ZITADEL resources on an organization level -- [Administration API](/apis/introduction#administration) - allows an admin or machine to manage the ZITADEL resources on an instance level -- [System API](/apis/introduction#system) - allows to create and change new ZITADEL instances +- [Management API](/apis/introduction#management) - allows an admin or machine to manage the Zitadel resources on an organization level +- [Administration API](/apis/introduction#administration) - allows an admin or machine to manage the Zitadel resources on an instance level +- [System API](/apis/introduction#system) - allows to create and change new Zitadel instances - [Asset API](/apis/introduction#assets) - is used to upload and download static assets ### Core Layer @@ -61,7 +61,7 @@ The Command Side has some unique requirements, these include: ##### Command Handler -The command handler receives all operations who alter a resource managed by ZITADEL. +The command handler receives all operations who alter a resource managed by Zitadel. For example if a user changes his name. The API Layer will pass the instruction received through the API call to the command handler for further processing. The command handler is then responsible of creating the necessary commands. After creating the commands the command hand them down to the command validation. @@ -75,14 +75,14 @@ These events now are being handed down to the storage layer for storage. #### Events -ZITADEL handles events in two ways. +Zitadel handles events in two ways. Events that should be processed in near real time are processed by a in memory pub sub system. Some events can be handled asynchronously using the spooler. ##### Pub Sub The pub sub system job is it to keep a query view up-to-date by feeding a constant stream of events to the projections. -Our pub sub system built into ZITADEL works by placing events into an in memory queue for its subscribers. +Our pub sub system built into Zitadel works by placing events into an in memory queue for its subscribers. There is no need for specific guarantees from the pub sub system. Since the SOR is the ES everything can be retried without loss of data. In case of an error an event can be reapplied in two ways: @@ -90,8 +90,8 @@ In case of an error an event can be reapplied in two ways: - The spooler takes care of background cleanups in a scheduled fashion > The decision to incorporate an internal pub sub system with no need for specific guarantees is a deliberate choice. -> We believe that the toll of operating an additional external service like a MQ system negatively affects the ease of use of ZITADEL as well as its availability guarantees. -> One of the authors of ZITADEL did his thesis to test this approach against established MQ systems. +> We believe that the toll of operating an additional external service like a MQ system negatively affects the ease of use of Zitadel as well as its availability guarantees. +> One of the authors of Zitadel did his thesis to test this approach against established MQ systems. ##### Spooler @@ -136,12 +136,16 @@ It is also responsible to execute authorization checks. To check if a request is ### Storage Layer -As ZITADEL itself is built completely stateless only the storage layer is needed to persist states. -The storage layer of ZITADEL is responsible for multiple tasks. For example: +As Zitadel itself is built completely stateless only the storage layer is needed to persist states. +The storage layer of Zitadel is responsible for multiple tasks. For example: - Guarantee strong consistency for the command side - Guarantee good query performance for the query side - Backup and restore operation for disaster recovery purpose -ZITADEL currently supports PostgreSQL and CockroachDB.. +Zitadel currently supports PostgreSQL. Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide on using one of them. + +:::info +Zitadel v2 supported CockroachDB and PostgreSQL. Zitadel v3 only supports PostgreSQL. Please refer to [the mirror guide](cli/mirror) to migrate to PostgreSQL. +::: \ No newline at end of file diff --git a/docs/docs/concepts/architecture/solution.md b/docs/docs/concepts/architecture/solution.md index b99b8aa9dc..49e9e8f62f 100644 --- a/docs/docs/concepts/architecture/solution.md +++ b/docs/docs/concepts/architecture/solution.md @@ -1,21 +1,20 @@ --- -title: ZITADEL's Deployment Architecture +title: Zitadel's Deployment Architecture sidebar_label: Deployment Architecture --- ## High Availability -ZITADEL can be run as high available system with ease. +Zitadel can be run as high available system with ease. Since the storage layer takes the heavy lifting of making sure that data in synched across, server, data centers or regions. -Depending on your projects needs our general recommendation is to run ZITADEL and ZITADELs storage layer across multiple availability zones in the same region or if you need higher guarantees run the storage layer across multiple regions. -Consult the [CockroachDB documentation](https://www.cockroachlabs.com/docs/) for more details or use the [CockroachCloud Service](https://www.cockroachlabs.com/docs/cockroachcloud/create-an-account.html) -Alternatively you can run ZITADEL also with Postgres which is [Enterprise Supported](/docs/support/software-release-cycles-support#partially-supported). -Make sure to read our [Production Guide](/self-hosting/manage/production#prefer-postgresql) before you decide to use it. +Depending on your projects needs our general recommendation is to run Zitadel across multiple availability zones in the same region or across multiple regions. +Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide to use it. +Consult the [Postgres documentation](https://www.postgresql.org/docs/) for more details. ## Scalability -ZITADEL can be scaled in a linear fashion in multiple dimensions. +Zitadel can be scaled in a linear fashion in multiple dimensions. - Vertical on your compute infrastructure - Horizontal in a region @@ -23,45 +22,38 @@ ZITADEL can be scaled in a linear fashion in multiple dimensions. Our customers can reuse the same already known binary or container and scale it across multiple server, data center and regions. To distribute traffic an already existing proxy infrastructure can be reused. -Simply steer traffic by path, hostname, IP address or any other metadata to the ZITADEL of your choice. +Simply steer traffic by path, hostname, IP address or any other metadata to the Zitadel of your choice. -> To improve your service quality we recommend steering traffic by path to different ZITADEL deployments +> To improve your service quality we recommend steering traffic by path to different Zitadel deployments > Feel free to [contact us](https://zitadel.com/contact/) for details ## Example Deployment Architecture ### Single Cluster / Region -A ZITADEL Cluster is a highly available IAM system with each component critical for serving traffic laid out at least three times. -As our storage layer (CockroachDB) relies on Raft, it is recommended to operate odd numbers of storage nodes to prevent "split brain" problems. -Hence our reference design for Kubernetes is to have three application nodes and three storage nodes. +A Zitadel Cluster is a highly available IAM system with each component critical for serving traffic laid out at least three times. +Our storage layer (Postgres) is built for single region deployments. +Hence our reference design for Kubernetes is to have three application nodes and one storage node. -> If you are using a serverless offering like Google Cloud Run you can scale ZITADEL from 0 to 1000 Pods without the need of deploying the node across multiple availability zones. - -:::info -CockroachDB needs to be configured with locality flags to proper distribute data over the zones -::: +> If you are using a serverless offering like Google Cloud Run you can scale Zitadel from 0 to 1000 Pods without the need of deploying the node across multiple availability zones. ![Cluster Architecture](/img/zitadel_cluster_architecture.png) ### Multi Cluster / Region -To scale ZITADEL across regions it is recommend to create at least three cluster. -We recommend to run an odd number of storage clusters (storage nodes per data center) to compensate for "split brain" scenarios. -In our reference design we recommend to create one cluster per region or cloud provider with a minimum of three regions. +To scale Zitadel across regions it is recommend to create at least three clusters. +Each cluster is a fully independent ZITADEL setup. +To keep the data in sync across all clusters, we recommend using Postgres with read-only replicas as a storage layer. +Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide to use it. +Consult the [Postgres documentation](https://www.postgresql.org/docs/current/high-availability.html) for more details. -With this design even the outage of a whole data-center would have a minimal impact as all data is still available at the other two locations. - -:::info -CockroachDB needs to be configured with locality flags to proper distribute data over the zones -::: ![Multi-Cluster Architecture](/img/zitadel_multicluster_architecture.png) ## Zero Downtime Updates Since an Identity system tends to be a critical piece of infrastructure, the "in place zero downtime update" is a well needed feature. -ZITADEL is built in a way that upgrades can be executed without downtime by just updating to a more recent version. +Zitadel is built in a way that upgrades can be executed without downtime by just updating to a more recent version. The common update involves the following steps and do not need manual intervention of the operator: @@ -78,5 +70,5 @@ Users who use [Kubernetes/Helm](/docs/self-hosting/deploy/kubernetes) or serverl :::info As a good practice we recommend creating Database Backups prior to an update. It is also recommend to read the release notes on GitHub before upgrading. -Since ZITADEL utilizes Semantic Versioning Breaking Changes of any kind will always increase the major version (e.g Version 2 would become Version 3). +Since Zitadel utilizes Semantic Versioning Breaking Changes of any kind will always increase the major version (e.g Version 2 would become Version 3). ::: diff --git a/docs/docs/concepts/features/actions_v2.md b/docs/docs/concepts/features/actions_v2.md index a06384639d..1617081222 100644 --- a/docs/docs/concepts/features/actions_v2.md +++ b/docs/docs/concepts/features/actions_v2.md @@ -10,6 +10,12 @@ This is useful when you have special business requirements that ZITADEL doesn't We're working on Actions continuously. In the [roadmap](https://zitadel.com/roadmap), you see how we are planning to expand and improve it. Please tell us about your needs and help us prioritize further fixes and features. ::: +:::warning +To use Actions v2 activate the feature flag "Actions" [feature flag](/docs/apis/resources/feature_service_v2/feature-service-set-instance-features), to be able to manage the related resources. + +The Actions v2 will always be executed if available, even if the feature flag is switched off, to remove any Actions v2 the related Execution has to be removed. +::: + ## Why actions? ZITADEL can't anticipate and solve every possible business rule and integration requirements from all ZITADEL users. Here are some examples: - A business requires domain specific data validation before a user can be created or authenticated. @@ -31,10 +37,25 @@ so that everybody can implement their custom behaviour for as many processes as Possible conditions for the Execution: - Request, to react to or manipulate requests to ZITADEL, for example add information to newly created users - Response, to react to or manipulate responses to ZITADEL, for example to provision newly created users to other systems -- Function, to react to different functionality in ZITADEL, replaces [Actions](/concepts/features/actions) +- Function, to react to different functionality in ZITADEL, replaces [Actions](/concepts/features/actions). - Event, to create to different events which get created in ZITADEL, for example to inform somebody if a user gets locked +:::info +Currently, the defined Actions v2 will be executed additionally to the defined [Actions](/concepts/features/actions). +::: + +## Migration + +- [Migrate Actions v1 to Actions v2](/guides/integrate/actions/migrate-from-v1) + ## Further reading -- [Actions v2 reference](/apis/actions/v3/usage) -- [Actions v2 example execution locally](/apis/actions/v3/testing-locally) \ No newline at end of file +- [Actions v2 reference](/guides/integrate/actions/usage) +- [Actions v2 example execution for request](/guides/integrate/actions/testing-request) +- [Actions v2 example execution for request manipulation](/guides/integrate/actions/testing-request-manipulation) +- [Actions v2 example execution for request signature check](/guides/integrate/actions/testing-request-signature) +- [Actions v2 example execution for response](/guides/integrate/actions/testing-response) +- [Actions v2 example execution for response manipulation](/guides/integrate/actions/testing-response-manipulation) +- [Actions v2 example execution for function](/guides/integrate/actions/testing-function) +- [Actions v2 example execution for function manipulation](/guides/integrate/actions/testing-function-manipulation) +- [Actions v2 example execution for event](/guides/integrate/actions/testing-event) \ No newline at end of file diff --git a/docs/docs/guides/integrate/actions/migrate-from-v1.md b/docs/docs/guides/integrate/actions/migrate-from-v1.md new file mode 100644 index 0000000000..2ea2b3f1e3 --- /dev/null +++ b/docs/docs/guides/integrate/actions/migrate-from-v1.md @@ -0,0 +1,114 @@ +--- +title: Migrate from Actions v1 to v2 +--- + +In this guide, you will have all necessary information to migrate from Actions v1 to Actions v2 with all currently [available Flow Types](/apis/actions/introduction#available-flow-types). + +## Internal Authentication + +### Post Authentication + +A user has authenticated directly at ZITADEL. +ZITADEL validated the users inputs for password, one-time password, security key or passwordless factor. + +To react to different authentication actions, the session service, `zitadel.session.v2.SessionService`, provides the different endpoints. As a rule of thumb, use response triggers if you primarily want to handle successful and failed authentications. On the other hand, use event triggers if you need more fine-granular handling, for example by the used authentication factors. + +Some use-cases: + +- Handle successful authentication through the response of `/zitadel.session.v2.SessionService/CreateSession` and `/zitadel.session.v2.SessionService/SetSession`, [Action Response Example](./testing-response) +- Handle failed authentication through the response of `/zitadel.session.v2.SessionService/CreateSession` and `/zitadel.session.v2.SessionService/SetSession`, [Action Response Example](./testing-response) +- Handle session with password checked through the creation of event `session.password.checked`, [Action Event Example](./testing-event) +- Handle successful authentication through the creation of event `user.human.password.check.succeeded`, [Action Event Example](./testing-event) +- Handle failed authentication through the creation of event `user.human.password.check.failed`, [Action Event Example](./testing-event) + +### Pre Creation + +A user registers directly at ZITADEL. +ZITADEL did not create the user yet. + +Some use-cases: + +- Before a user is created through the request on `/zitadel.user.v2.UserService/AddHumanUser`, [Action Request Example](./testing-request) +- Add information to the user through the request on `/zitadel.user.v2.UserService/AddHumanUser`, [Action Request Manipulation Example](./testing-request-manipulation) + +### Post Creation + +A user registers directly at ZITADEL. +ZITADEL successfully created the user. + +Some use-cases: + +- After user is created through the response on `/zitadel.user.v2.UserService/AddHumanUser`, [Action Response Example](./testing-response) +- At the event of a user creation on `user.human.added`, [Action Event Example](./testing-event) + +## External Authentication + +### Post Authentication + +A user has authenticated externally. ZITADEL retrieved and mapped the external information. + +Some use-cases: + +- Handle the information mapping from the external authentication to internal structure through the response on `/zitadel.user.v2.UserService/RetrieveIdentityProviderIntent`, [Action Response Example](./testing-response) + - information about the link to the external IDP available in the response under [`idpInformation`](/apis/resources/user_service_v2/user-service-retrieve-identity-provider-intent) + - information if a new user has to be created available in the response under [`addHumanUser`](/apis/resources/user_service_v2/user-service-retrieve-identity-provider-intent), including metadata and link to external IDP + +### Pre Creation + +A user registers directly at ZITADEL. +ZITADEL did not create the user yet. + +Some use-cases: + +- Before a user is created through the request on `/zitadel.user.v2.UserService/AddHumanUser`, [Action Request Example](./testing-request) +- Add information to the user through the request on `/zitadel.user.v2.UserService/AddHumanUser`, [Action Request Manipulation Example](./testing-request-manipulation) + +### Post Creation + +A user registers directly at ZITADEL. +ZITADEL successfully created the user. + +Some use-cases: + +- After user is created through the response on `/zitadel.user.v2.UserService/AddHumanUser`, [Action Response Example](./testing-response) +- At the event of a user creation on `user.human.added`, [Action Event Example](./testing-event) + +## Complement Token + +These are executed during the creation of tokens and token introspection. + +### Pre Userinfo + +These are called before userinfo are set in the id_token or userinfo and introspection endpoint response. + +Some use-cases: + +- Add claims to the userinfo through function on `preuserinfo`, [Action Function Example](./testing-function) +- Add metadata to user through function on `preuserinfo`, [Action Function Example](./testing-function) +- Add logs to the log claim through function on `preuserinfo`, [Action Function Example](./testing-function) + +### Pre Access Token + +These are called before the claims are set in the access token and the token type is `jwt`. + +Some use-cases: + +- Add claims to the userinfo through function on `preaccesstoken`, [Action Function Example](./testing-function) +- Add metadata to user through function on `preaccesstoken`, [Action Function Example](./testing-function) +- Add logs to the log claim through function on `preaccesstoken`, [Action Function Example](./testing-function) + +## Customize SAML Response + +These are executed before the return of the SAML Response. + +### Pre SAMLResponse Creation + +These are called before attributes are set in the SAMLResponse. + +Some use-cases: + +- Add custom attributes to the response through function on `presamlresponse`, [Action Function Example](./testing-function) +- Add metadata to user through function on `presamlresponse`, [Action Function Example](./testing-function) + + + diff --git a/docs/docs/guides/integrate/actions/testing-event.md b/docs/docs/guides/integrate/actions/testing-event.md new file mode 100644 index 0000000000..8b4502703b --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-event.md @@ -0,0 +1,176 @@ +--- +title: Test Actions Event +--- + +This guide shows you how to leverage the ZITADEL actions feature to react to events in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific event occurs. +This is useful for integrating with other systems or for triggering workflows based on events in ZITADEL. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +## Start example target + +To test the actions feature, you need to create a target that will be called when an event occurs. +You will need to implement a listener that can receive HTTP requests and process the events. +For this example, we will use a simple Go HTTP server that will print the received events to standard output. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "fmt" + "io" + "net/http" +) + +// webhook HandleFunc to read the request body and then print out the contents +func webhook(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + defer req.Body.Close() + // print out the read content + fmt.Println(string(sentBody)) +} + +func main() { + // handle the HTTP call under "/webhook" + http.HandleFunc("/webhook", webhook) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the +target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local webhook", + "restWebhook": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/webhook", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To configure ZITADEL to call the target when an event occurs, you need to set an execution and define the event +condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "event": { + "event": "user.human.added" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by creating a user through the Console UI or +by calling the ZITADEL API to create a human user. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "userId": { + "givenName": "Test", + "familyName": "User" + }, + "email": { + "email": "example@test.com" + } +}' +``` + +Your server should now print out something like the following. Check out +the [Sent information Event](./usage#sent-information-event) payload description. + +```json +{ + "aggregateID": "313014806065971608", + "aggregateType": "user", + "resourceOwner": "312909075211944344", + "instanceID": "312909075211878808", + "version": "v2", + "sequence": 1, + "event_type": "user.human.added", + "created_at": "2025-03-27T10:22:43.262665+01:00", + "userID": "312909075212468632", + "event_payload": { + "userName":"example@test.com", + "firstName":"Test", + "lastName":"User", + "displayName":"Test User", + "preferredLanguage":"und", + "email":"example@test.com" + } +} +``` + +The event_payload is base64 encoded and has the following content: + +```json +{ + "userName": "example@test.com", + "firstName": "Test", + "lastName": "User", + "displayName": "Test User", + "preferredLanguage": "und", + "email": "example@test.com" +} +``` + +## Conclusion + +You have successfully set up a target and execution to react to events in your ZITADEL instance. +This feature can now be used to integrate with your existing systems to create custom workflows or automate tasks based on events in ZITADEL. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). \ No newline at end of file diff --git a/docs/docs/guides/integrate/actions/testing-function-manipulation.md b/docs/docs/guides/integrate/actions/testing-function-manipulation.md new file mode 100644 index 0000000000..8f5e2fc968 --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-function-manipulation.md @@ -0,0 +1,155 @@ +--- +title: Test Actions Function Manipulation +--- + +This guide shows you how to leverage the ZITADEL actions feature to enhance different functions in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific functionality is used. +This is useful for integrating with other systems which need specific claims in tokens or for executing external code during OIDC or SAML flows. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +## Available functions + +The available conditions can be found under [all available Functions](/apis/resources/action_service_v2/action-service-list-execution-functions). + +## Start example target + +To test the actions feature, you need to create a target that will be called when a function is used. +You will need to implement a listener that can receive HTTP requests and process the data. +For this example, we will use a simple Go HTTP server that will send back static data. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "encoding/json" + "net/http" +) + +type Response struct { + SetUserMetadata []*Metadata `json:"set_user_metadata,omitempty"` + AppendClaims []*AppendClaim `json:"append_claims,omitempty"` + AppendLogClaims []string `json:"append_log_claims,omitempty"` +} + +type Metadata struct { + Key string `json:"key"` + Value []byte `json:"value"` +} + +type AppendClaim struct { + Key string `json:"key"` + Value any `json:"value"` +} + +// call HandleFunc to respond with static data +func call(w http.ResponseWriter, req *http.Request) { + // create the response with the correct structure + resp := &Response{ + SetUserMetadata: []*Metadata{ + {Key: "key", Value: []byte("value")}, + }, + AppendClaims: []*AppendClaim{ + {Key: "claim", Value: "value"}, + }, + AppendLogClaims: []string{"log1", "log2", "log3"}, + } + data, err := json.Marshal(resp) + if err != nil { + // if there was an error while marshalling the json + http.Error(w, "error", http.StatusInternalServerError) + return + } + w.Write(data) +} + +func main() { + // handle the HTTP call under "/call" + http.HandleFunc("/call", call) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} + +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as call, the target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local call", + "restCall": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/call", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To configure ZITADEL to call the target when a function is executed, you need to set an execution and define the function +condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "function": { + "name": "preuserinfo" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by logging into Console UI or +by using any OIDC flow. + +As a result 3 things happen: +- the user get the metadata with the key "key" and value "value" added +- the token has a claim "urn:zitadel:iam:claim" added with value "value" +- the token has the log claim "urn:zitadel:iam:action:preuserinfo:log" added with values "log1", "log2" and "log3". + +For any further information related to [the OIDC Flow, refer to our documentation.](/guides/integrate/login/oidc/login-users) + +## Conclusion + +You have successfully set up a target and execution to react to functions in your ZITADEL instance. +This feature can now be used to integrate with your existing systems to create custom workflows or automate tasks based on functionality in ZITADEL. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/guides/integrate/actions/testing-function.md b/docs/docs/guides/integrate/actions/testing-function.md new file mode 100644 index 0000000000..f14f20b69d --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-function.md @@ -0,0 +1,171 @@ +--- +title: Test Actions Function +--- + +This guide shows you how to leverage the ZITADEL actions feature to enhance different functions in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific functionality is used. +This is useful for integrating with other systems which need specific claims in tokens or for executing external code during OIDC or SAML flows. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +## Available functions + +The available conditions can be found under [all available Functions](/apis/resources/action_service_v2/action-service-list-execution-functions). + +## Start example target + +To test the actions feature, you need to create a target that will be called when a function is used. +You will need to implement a listener that can receive HTTP requests and process the data. +For this example, we will use a simple Go HTTP server that will print the received data to standard output. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "fmt" + "io" + "net/http" +) + +// webhook HandleFunc to read the request body and then print out the contents +func webhook(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + defer req.Body.Close() + // print out the read content + fmt.Println(string(sentBody)) +} + +func main() { + // handle the HTTP call under "/webhook" + http.HandleFunc("/webhook", webhook) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} + +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local call", + "restWebhook": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/webhook", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To configure ZITADEL to call the target when a function is executed, you need to set an execution and define the function +condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "function": { + "name": "preuserinfo" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by logging into Console UI or +by using any OIDC flow. + +Your server should now print out something like the following. Check out the [Sent information Function](./usage#sent-information-function) payload description. +```json +{ + "function" : "function/preuserinfo", + "userinfo" : { + "sub" : "312909075212468632" + }, + "user" : { + "id" : "312909075212468632", + "creation_date" : "2025-03-26T15:52:23.917636Z", + "change_date" : "2025-03-26T15:52:23.917636Z", + "resource_owner" : "312909075211944344", + "sequence" : 2, + "state" : 1, + "username" : "user@example.com", + "preferred_login_name" : "zitadel@zitadel.localhost", + "human" : { + "first_name" : "Example firstname", + "last_name" : "Example lastname", + "display_name" : "Example displayname", + "preferred_language" : "en", + "email" : "user@example.com", + "is_email_verified" : true, + "password_changed" : "0001-01-01T00:00:00Z", + "mfa_init_skipped" : "0001-01-01T00:00:00Z" + } + }, + "user_metadata" : [ { + "creation_date" : "2025-03-27T09:10:25.879677Z", + "change_date" : "2025-03-27T09:10:25.879677Z", + "resource_owner" : "312909075211944344", + "sequence" : 18, + "key" : "key", + "value" : "dmFsdWU=" + } ], + "org" : { + "id" : "312909075211944344", + "name" : "ZITADEL", + "primary_domain" : "example.com" + } +} +``` + +For any further information related to [the OIDC Flow, refer to our documentation.](/guides/integrate/login/oidc/login-users) + +## Conclusion + +You have successfully set up a target and execution to react to functions in your ZITADEL instance. +This feature can now be used to customize the functionality in ZITADEL, in particular the content of the OIDC tokens and SAML responses. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/guides/integrate/actions/testing-request-manipulation.md b/docs/docs/guides/integrate/actions/testing-request-manipulation.md new file mode 100644 index 0000000000..1cb4f1776a --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-request-manipulation.md @@ -0,0 +1,211 @@ +--- +title: Test Actions Request Manipulation +--- + +This guide shows you how to leverage the ZITADEL actions feature to manipulate API requests in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific API request occurs. +This is useful for adding information to managed resources in ZITADEL. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +:::warning +To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request is a protocol buffer message, to avoid potential problems with the attribute names. +::: + +## Start example target + +To test the actions feature, you need to create a target that will be called when an API endpoint is called. +You will need to implement a listener that can receive HTTP requests, process the request and returns the manipulated request. +For this example, we will use a simple Go HTTP server that will return the request with added metadata. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "google.golang.org/protobuf/encoding/protojson" +) + +type contextRequest struct { + Request *addHumanUserRequestWrapper `json:"request"` +} + +// addHumanUserRequestWrapper necessary to marshal and unmarshal the JSON into the proto message correctly +type addHumanUserRequestWrapper struct { + user.AddHumanUserRequest +} + +func (r *addHumanUserRequestWrapper) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *addHumanUserRequestWrapper) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r) +} + +// call HandleFunc to read the request body, manipulate the content and return the manipulated request +func call(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + defer req.Body.Close() + + // read the request into the expected structure + request := new(contextRequest) + if err := json.Unmarshal(sentBody, request); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + } + + // build the response from the received request + response := request.Request + // manipulate the request to send back as response + if response.Metadata == nil { + response.Metadata = make([]*user.SetMetadataEntry, 0) + } + response.Metadata = append(response.Metadata, &user.SetMetadataEntry{Key: "organization", Value: []byte("company")}) + + // marshal the request into json + data, err := json.Marshal(response) + if err != nil { + // if there was an error while marshalling the json + http.Error(w, "error", http.StatusInternalServerError) + return + } + + // return the manipulated request + w.Write(data) +} + +func main() { + // handle the HTTP call under "/call" + http.HandleFunc("/call", call) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} + +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as call, the target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local call", + "restCall": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/call", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To call the target just created before, with the intention to manipulate the request used for user creation by the user V2 API, we define an execution with a method condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "request": { + "method": "/zitadel.user.v2.UserService/AddHumanUser" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by creating a user through the Console UI or +by calling the ZITADEL API to create a human user. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "profile": { + "givenName": "Example_given", + "familyName": "Example_family" + }, + "email": { + "email": "example@example.com" + } +}' +``` + +Your server should now manipulate the request to something like the following. Check out +the [Sent information Request](./usage#sent-information-request) payload description. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "profile": { + "givenName": "Example_given", + "familyName": "Example_family" + }, + "email": { + "email": "example@example.com" + } + "metadata": [ + {"key": "organization", "value": "Y29tcGFueQ=="} + ] +}' +``` + +## Conclusion + +You have successfully set up a target and execution to manipulate API requests in your ZITADEL instance. +This feature can now be used to add or manipulate information to managed resources in ZITADEL. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/apis/actions/v3/testing-locally.md b/docs/docs/guides/integrate/actions/testing-request-signature.md similarity index 51% rename from docs/docs/apis/actions/v3/testing-locally.md rename to docs/docs/guides/integrate/actions/testing-request-signature.md index b5b3cb389f..c1932a7d5b 100644 --- a/docs/docs/apis/actions/v3/testing-locally.md +++ b/docs/docs/guides/integrate/actions/testing-request-signature.md @@ -1,8 +1,10 @@ --- -title: Test Actions Locally +title: Test Actions Request Signature Check --- -In this guide, you will create a ZITADEL execution and target. After a user is created through the API, the target is called. +This guide shows you how to leverage the ZITADEL actions feature to react to API requests in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific API request occurs. +This is useful for information provisioning in between systems or for triggering workflows based on API requests in ZITADEL. ## Prerequisites @@ -11,9 +13,22 @@ Before you start, make sure you have everything set up correctly. - You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) - Your ZITADEL instance needs to have the actions feature enabled. +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +:::warning +To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request is a protocol buffer message, to avoid potential problems with the attribute names. +::: + ## Start example target -To start a simple HTTP server locally, which receives the webhook call, the following code example can be used: +To test the actions feature, you need to create a target that will be called when an API endpoint is called. +You will need to implement a listener that can receive HTTP requests, check the signature and process the request. +For this example, we will use a simple Go HTTP server that will print the received request to standard output. +The 'signingKey' is the key received in the next step 'Create target'. ```go package main @@ -22,8 +37,12 @@ import ( "fmt" "io" "net/http" + + "github.com/zitadel/zitadel/pkg/actions" ) +const signingKey = "somekey" + // webhook HandleFunc to read the request body and then print out the contents func webhook(w http.ResponseWriter, req *http.Request) { // read the body content @@ -33,6 +52,13 @@ func webhook(w http.ResponseWriter, req *http.Request) { http.Error(w, "error", http.StatusInternalServerError) return } + defer req.Body.Close() + // validate signature + if err := actions.ValidatePayload(sentBody, req.Header.Get(actions.SigningHeader), signingKey); err != nil { + // if the signed content is not equal the sent content return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } // print out the read content fmt.Println(string(sentBody)) } @@ -46,30 +72,14 @@ func main() { } ``` -What happens here is only a target which prints out the received request, which could also be handled with a different logic. - -### Check Signature - -To additionally check the signature header you can add the following to the example: -```go - // validate signature - if err := actions.ValidatePayload(sentBody, req.Header.Get(actions.SigningHeader), signingKey); err != nil { - // if the signed content is not equal the sent content return an error - http.Error(w, "error", http.StatusInternalServerError) - return - } -``` - -Where you can replace 'signingKey' with the key received in the next step 'Create target'. - ## Create target As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows: -[Create a target](/apis/resources/action_service_v3/zitadel-actions-create-target) +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. ```shell -curl -L -X POST 'https://$CUSTOM-DOMAIN/v3alpha/targets' \ +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -87,12 +97,13 @@ Save the returned ID to set in the execution. ## Set execution -To call the target just created before, with the intention to print the request used for user creation by the user V2 API, we define an execution with a method condition. +To configure ZITADEL to call the target when an API endpoint is called, you need to set an execution and define the request +condition. -[Set an execution](/apis/resources/action_service_v3/zitadel-actions-set-execution) +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. ```shell -curl -L -X PUT 'https://$CUSTOM-DOMAIN/v3alpha/executions' \ +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -112,7 +123,8 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v3alpha/executions' \ ## Example call -Now on every call on `/zitadel.user.v2.UserService/AddHumanUser` the local server prints out the received body of the request: +Now that you have set up the target and execution, you can test it by creating a user through the Console UI or +by calling the ZITADEL API to create a human user. ```shell curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ @@ -130,7 +142,9 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ }' ``` -Should print out something like, also described under [Sent information Request](./usage#sent-information-request): +Your server should now print out something like the following. Check out +the [Sent information Request](./usage#sent-information-request) payload description. + ```shell { "fullMethod": "/zitadel.user.v2.UserService/AddHumanUser", @@ -150,4 +164,9 @@ Should print out something like, also described under [Sent information Request] } ``` +## Conclusion +You have successfully set up a target and execution to react to API requests in your ZITADEL instance. +This feature can now be used to provision information in between systems or for triggering workflows based on API requests in ZITADEL. +Additionally, you are sure that the request was not tempered with, as the signature was created with the combination of signing key and payload. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/guides/integrate/actions/testing-request.md b/docs/docs/guides/integrate/actions/testing-request.md new file mode 100644 index 0000000000..b2413e606e --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-request.md @@ -0,0 +1,164 @@ +--- +title: Test Actions Request +--- + +This guide shows you how to leverage the ZITADEL actions feature to react to API requests in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific API request occurs. +This is useful for information provisioning in between systems or for triggering workflows based on API requests in ZITADEL. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +:::warning +To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request is a protocol buffer message, to avoid potential problems with the attribute names. +::: + +## Start example target + +To test the actions feature, you need to create a target that will be called when an API endpoint is called. +You will need to implement a listener that can receive HTTP requests and process the request. +For this example, we will use a simple Go HTTP server that will print the received request to standard output. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "fmt" + "io" + "net/http" +) + +// webhook HandleFunc to read the request body and then print out the contents +func webhook(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + defer req.Body.Close() + // print out the read content + fmt.Println(string(sentBody)) +} + +func main() { + // handle the HTTP call under "/webhook" + http.HandleFunc("/webhook", webhook) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local webhook", + "restWebhook": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/webhook", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To configure ZITADEL to call the target when an API endpoint is called, you need to set an execution and define the request +condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "request": { + "method": "/zitadel.user.v2.UserService/AddHumanUser" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by creating a user through the Console UI or +by calling the ZITADEL API to create a human user. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "profile": { + "givenName": "Test", + "familyName": "User" + }, + "email": { + "email": "example@test.com" + } +}' +``` + +Your server should now print out something like the following. Check out +the [Sent information Request](./usage#sent-information-request) payload description. + +```shell +{ + "fullMethod": "/zitadel.user.v2.UserService/AddHumanUser", + "instanceID": "262851882718855632", + "orgID": "262851882718921168", + "projectID": "262851882719052240", + "userID": "262851882718986704", + "request": { + "profile": { + "given_name": "Test", + "family_name": "User" + }, + "email": { + "email": "example@test.com" + } + } +} +``` + +## Conclusion + +You have successfully set up a target and execution to react to API requests in your ZITADEL instance. +This feature can now be used to provision information in between systems or for triggering workflows based on API requests in ZITADEL. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/guides/integrate/actions/testing-response-manipulation.md b/docs/docs/guides/integrate/actions/testing-response-manipulation.md new file mode 100644 index 0000000000..9d95479b05 --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-response-manipulation.md @@ -0,0 +1,294 @@ +--- +title: Test Actions Response Manipulation +--- + +This guide shows you how to leverage the ZITADEL actions feature to manipulate API responses in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific API response occurs. +This is useful for triggering workflows based on API responses in ZITADEL. You can even use this to provide data necessary data to the new login UI as shown in this example. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +:::warning +To marshal and unmarshal the request and response please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request and response are protocol buffer messages, to avoid potential problems with the attribute names. +::: + +## Start example target + +To test the actions feature, you need to create a target that will be called when an API endpoint is called. +You will need to implement a listener that can receive HTTP requests, process the request and returns the manipulated request. +For this example, we will use a simple Go HTTP server that will return the request with added metadata. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "google.golang.org/protobuf/encoding/protojson" +) + +type contextResponse struct { + Request *retrieveIdentityProviderIntentRequestWrapper `json:"request"` + Response *retrieveIdentityProviderIntentResponseWrapper `json:"response"` +} + +// RetrieveIdentityProviderIntentRequestWrapper necessary to marshal and unmarshal the JSON into the proto message correctly +type retrieveIdentityProviderIntentRequestWrapper struct { + user.RetrieveIdentityProviderIntentRequest +} + +func (r *retrieveIdentityProviderIntentRequestWrapper) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *retrieveIdentityProviderIntentRequestWrapper) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r) +} + +// RetrieveIdentityProviderIntentResponseWrapper necessary to marshal and unmarshal the JSON into the proto message correctly +type retrieveIdentityProviderIntentResponseWrapper struct { + user.RetrieveIdentityProviderIntentResponse +} + +func (r *retrieveIdentityProviderIntentResponseWrapper) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *retrieveIdentityProviderIntentResponseWrapper) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r) +} + +// call HandleFunc to read the response body, manipulate the content and return the response +func call(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + defer req.Body.Close() + + // read the response into the expected structure + request := new(contextResponse) + if err := json.Unmarshal(sentBody, request); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + } + + // build the response from the received response + resp := request.Response + // manipulate the received response to send back as response + if resp != nil && resp.AddHumanUser != nil { + // manipulate the response + resp.AddHumanUser.Metadata = append(resp.AddHumanUser.Metadata, &user.SetMetadataEntry{Key: "organization", Value: []byte("company")}) + } + + // marshal the response into json + data, err := json.Marshal(resp) + if err != nil { + // if there was an error while marshalling the json + http.Error(w, "error", http.StatusInternalServerError) + return + } + + // return the manipulated response + w.Write(data) +} + +func main() { + // handle the HTTP call under "/call" + http.HandleFunc("/call", call) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as call, the +target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local call", + "restCall": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/call", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To call the target just created before, with the intention to manipulate the retrieve of an intent by the user V2 API, +we define an execution with a response condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "response": { + "method": "/zitadel.user.v2.UserService/RetrieveIdentityProviderIntent" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by using a login-flow in the typescript login with an external IDP. + +```json +{ + "details": { + "sequence": "599", + "changeDate": "2023-06-15T06:44:26.039444Z", + "resourceOwner": "163840776835432705" + }, + "idpInformation": { + "oauth": { + "accessToken": "ya29...", + "idToken": "ey..." + }, + "idpId": "218528353504723201", + "userId": "218528353504723202", + "username": "test-user@localhost", + "rawInformation": { + "User": { + "email": "test-user@localhost", + "email_verified": true, + "family_name": "User", + "given_name": "Test", + "hd": "mouse.com", + "locale": "de", + "name": "Minnie Mouse", + "picture": "https://lh3.googleusercontent.com/a/AAcKTtf973Q7NH8KzKTMEZELPU9lx45WpQ9FRBuxFdPb=s96-c", + "sub": "111392805975715856637" + } + } + }, + "addHumanUser": { + "idpLinks": [ + {"idpId": "218528353504723201", "userId": "218528353504723202", "userName": "test-user@localhost"} + ], + "username": "test-user@localhost", + "profile": { + "givenName": "Test", + "familyName": "User", + "displayName": "Test User", + "preferredLanguage": "de" + }, + "email": { + "email": "test-user@zitadel.ch", + "isVerified": true + }, + "metadata": [] + } +} +``` + +Your server should now manipulate the response to something like the following. Check out +the [Sent information Response](./usage#sent-information-response) payload description. + +```json +{ + "details": { + "sequence": "599", + "changeDate": "2023-06-15T06:44:26.039444Z", + "resourceOwner": "163840776835432705" + }, + "idpInformation": { + "oauth": { + "accessToken": "ya29...", + "idToken": "ey..." + }, + "idpId": "218528353504723201", + "userId": "218528353504723202", + "username": "test-user@localhost", + "rawInformation": { + "User": { + "email": "test-user@localhost", + "email_verified": true, + "family_name": "User", + "given_name": "Test", + "hd": "mouse.com", + "locale": "de", + "name": "Minnie Mouse", + "picture": "https://lh3.googleusercontent.com/a/AAcKTtf973Q7NH8KzKTMEZELPU9lx45WpQ9FRBuxFdPb=s96-c", + "sub": "111392805975715856637" + } + } + }, + "addHumanUser": { + "idpLinks": [ + {"idpId": "218528353504723201", "userId": "218528353504723202", "userName": "test-user@localhost"} + ], + "username": "test-user@localhost", + "profile": { + "givenName": "Test", + "familyName": "User", + "displayName": "Test User", + "preferredLanguage": "de" + }, + "email": { + "email": "test-user@zitadel.ch", + "isVerified": true + }, + "metadata": [ + {"key": "organization", "value": "Y29tcGFueQ=="} + ] + } +} +``` + +## Conclusion + +You have successfully set up a target and execution to manipulate API responses in your ZITADEL instance. +This feature can now be used to add necessary information for clients including the new login UI. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/guides/integrate/actions/testing-response.md b/docs/docs/guides/integrate/actions/testing-response.md new file mode 100644 index 0000000000..a2ab736505 --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-response.md @@ -0,0 +1,172 @@ +--- +title: Test Actions Response +--- + +This guide shows you how to leverage the ZITADEL actions feature to react to API responses in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific API response occurs. +This is useful for information provisioning in between systems or for triggering workflows based on API responses in ZITADEL. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +:::warning +To marshal and unmarshal the request and response please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request and response are protocol buffer messages, to avoid potential problems with the attribute names. +::: + +## Start example target + +To test the actions feature, you need to create a target that will be called when an API endpoint is called. +You will need to implement a listener that can receive HTTP requests and process the request. +For this example, we will use a simple Go HTTP server that will print the received request to standard output. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "fmt" + "io" + "net/http" +) + +// webhook HandleFunc to read the request body and then print out the contents +func webhook(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + defer req.Body.Close() + // print out the read content + fmt.Println(string(sentBody)) +} + +func main() { + // handle the HTTP call under "/webhook" + http.HandleFunc("/webhook", webhook) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local webhook", + "restWebhook": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/webhook", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To configure Zitadel to call the target when an API endpoint is called, you need to set an execution and define the response +condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "response": { + "method": "/zitadel.user.v2.UserService/AddHumanUser" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by creating a user through the Console UI or +by calling the ZITADEL API to create a human user. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "userId": { + "givenName": "Example_given", + "familyName": "Example_family" + }, + "email": { + "email": "example@example.com" + } +}' +``` + +Your server should now print out something like the following. Check out +the [Sent information Response](./usage#sent-information-response) payload description. + +```json +{ + "fullMethod": "/zitadel.user.v2.UserService/AddHumanUser", + "instanceID": "262851882718855632", + "orgID": "262851882718921168", + "projectID": "262851882719052240", + "userID": "262851882718986704", + "request": { + "profile": { + "given_name": "Example_given", + "family_name": "Example_family" + }, + "email": { + "email": "example@example.com" + } + }, + "response": { + "user_id": "312918757460672920", + "details": { + "sequence": "2", + "change_date": "2025-03-26T17:28:33.856436Z", + "resource_owner": "312909075211944344", + } + } +} +``` + +## Conclusion + +You have successfully set up a target and execution to react to API responses in your ZITADEL instance. +This feature can now be used to provision information in between systems or for triggering workflows based on API responses in ZITADEL. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/guides/integrate/actions/usage.md b/docs/docs/guides/integrate/actions/usage.md new file mode 100644 index 0000000000..ba512ae549 --- /dev/null +++ b/docs/docs/guides/integrate/actions/usage.md @@ -0,0 +1,502 @@ +--- +title: Using Actions +--- + +The Action API provides a flexible mechanism for customizing and extending the functionality of ZITADEL. By allowing you to define targets and executions, you can implement custom workflows triggered on an API requests and responses, events or specific functions. + +**How it works:** +- Create Target +- Set Execution with condition and target +- Custom Code will be triggered and executed + +**Use Cases:** +- User Management: Automate provisioning user data to external systems when users are crreated, updated or deleted. +- Security: Implement IP blocking or rate limiting based on API usage patterns. +- Extend Workflows: Automatically setup resources in your application, when a new organization in ZITADEL is created. +- Token extension: Add custom claims to the tokens. + +## Endpoints + +ZITADEL sends an HTTP Post request to the endpoint set as Target, the received request than can be edited and send back or custom processes can be handled. + +### Sent information Request + +The information sent to the Endpoint is structured as JSON: + +```json +{ + "fullMethod": "full method of the GRPC call", + "instanceID": "instanceID of the called instance", + "orgID": "ID of the organization related to the calling context", + "projectID": "ID of the project related to the used application", + "userID": "ID of the calling user", + "request": { + "attribute": "Attribute value of full request of the call" + } +} +``` + +:::warning +To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request is a protocol buffer message, to avoid potential problems with the attribute names. +::: + +### Sent information Response + +The information sent to the Endpoint is structured as JSON: + +```json +{ + "fullMethod": "full method of the GRPC call", + "instanceID": "instanceID of the called instance", + "orgID": "ID of the organization related to the calling context", + "projectID": "ID of the project related to the used application", + "userID": "ID of the calling user", + "request": { + "attribute": "Attribute value of full request of the call" + }, + "response": { + "attribute": "Attribute value of full response of the call" + } +} +``` + +:::warning +To marshal and unmarshal the request and response please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request and response are protocol buffer messages, to avoid potential problems with the attribute names. +::: + +### Sent information Function + +Information sent and expected back are specific to the function. + +#### PreUserinfo + +The information sent to the Endpoint is structured as JSON: +```json +{ + "function": "Name of the function", + "userinfo": { + "given_name": "", + "family_name": "", + "middle_name": "", + "nickname": "", + "profile": "", + "picture": "", + ... + "preferred_username": "", + "email": "", + "email_verified": true, + "phone_number": "", + "phone_number_verified": true + }, + "user": { + "id": "", + "creation_date": "", + ... + "human": { + "first_name": "", + "last_name": "", + ... + "email": "", + "is_email_verified": true, + "phone": "", + "is_phone_verified": true + } + }, + "user_metadata": [ + { + "creation_date": "", + "change_date": "", + "resource_owner": "", + "sequence": "", + "key": "", + "value": "" + } + ], + "org": { + "id": "ID of the organization the user belongs to", + "name": "Name of the organization the user belongs to", + "primary_domain": "Primary domain of the organization the user belongs to" + }, + "user_grants": [ + { + "id": "", + "projectGrantId": "The ID of the project grant", + "state": 1, + "creationDate": "", + "changeDate": "", + "sequence": 1, + "userId": "", + "roles": [ + "role" + ], + "userResourceOwner": "The ID of the organization the user belongs to", + "userGrantResourceOwner": "The ID of the organization the user got authorization granted", + "userGrantResourceOwnerName": "The name of the organization the user got authorization granted", + "projectId": "", + "projectName": "" + } + ] +} +``` + +The expected structure of the JSON as response: + +```json +{ + "set_user_metadata": [ + { + "key": "key of metadata to be set on the user", + "value": "base64 value of metadata to be set on the user" + } + ], + "append_claims": [ + { + "key": "key of claim to be set on the user", + "value": "value of claim to be set on the user" + } + ], + "append_log_claims": [ + "Log to be appended to the log claim on the token" + ] +} +``` + +#### PreAccessToken + +The information sent to the Endpoint is structured as JSON: + +```json +{ + "function": "Name of the function", + "userinfo": { + "given_name": "", + "family_name": "", + "middle_name": "", + "nickname": "", + "profile": "", + "picture": "", + ... + "preferred_username": "", + "email": "", + "email_verified": true/false, + "phone_number": "", + "phone_number_verified": true/false + }, + "user": { + "id": "", + "creation_date": "", + ... + "human": { + "first_name": "", + "last_name": "", + ... + "email": "", + "is_email_verified": true, + "phone": "", + "is_phone_verified": true + } + }, + "user_metadata": [ + { + "creation_date": "", + "change_date": "", + "resource_owner": "", + "sequence": "", + "key": "", + "value": "" + } + ], + "org": { + "id": "ID of the organization the user belongs to", + "name": "Name of the organization the user belongs to", + "primary_domain": "Primary domain of the organization the user belongs to" + }, + "user_grants": [ + { + "id": "", + "projectGrantId": "The ID of the project grant", + "state": 1, + "creationDate": "", + "changeDate": "", + "sequence": 1, + "userId": "", + "roles": [ + "role" + ], + "userResourceOwner": "The ID of the organization the user belongs to", + "userGrantResourceOwner": "The ID of the organization the user got authorization granted", + "userGrantResourceOwnerName": "The name of the organization the user got authorization granted", + "projectId": "", + "projectName": "" + } + ] +} +``` + +The expected structure of the JSON as response: + +```json +{ + "set_user_metadata": [ + { + "key": "key of metadata to be set on the user", + "value": "base64 value of metadata to be set on the user" + } + ], + "append_claims": [ + { + "key": "key of claim to be set on the user", + "value": "value of claim to be set on the user" + } + ], + "append_log_claims": [ + "Log to be appended to the log claim on the token" + ] +} +``` + +#### PreSAMLResponse + +The information sent to the Endpoint is structured as JSON: +```json +{ + "function": "Name of the function", + "userinfo": { + "given_name": "", + "family_name": "", + "middle_name": "", + "nickname": "", + "profile": "", + "picture": "", + ... + "preferred_username": "", + "email": "", + "email_verified": true, + "phone_number": "", + "phone_number_verified": true + }, + "user": { + "id": "", + "creation_date": "", + ... + "human": { + "first_name": "", + "last_name": "", + ... + "email": "", + "is_email_verified": true, + "phone": "", + "is_phone_verified": true + } + }, + "user_grants": [ + { + "id": "", + "projectGrantId": "The ID of the project grant", + "state": 1, + "creationDate": "", + "changeDate": "", + "sequence": 1, + "userId": "", + "roles": [ + "role" + ], + "userResourceOwner": "The ID of the organization the user belongs to", + "userGrantResourceOwner": "The ID of the organization the user got authorization granted", + "userGrantResourceOwnerName": "The name of the organization the user got authorization granted", + "projectId": "", + "projectName": "" + } + ] +} +``` + +The expected structure of the JSON as response: + +```json +{ + "set_user_metadata": [ + { + "key": "key of metadata to be set on the user", + "value": "base64 value of metadata to be set on the user" + } + ], + "append_attribute": [ + { + "name": "name of the attribute to be added to the response", + "name_format": "name format of the attribute to be added to the response", + "value": "value of the attribute to be added to the response" + } + ] +} +``` + +### Sent information Event + +The information sent to the Endpoint is structured as JSON: + +```json +{ + "aggregateID": "ID of the aggregate", + "aggregateType": "Type of the aggregate", + "resourceOwner": "Resourceowner the aggregate belongs to", + "instanceID": "ID of the instance the aggregate belongs to", + "version": "Version of the aggregate", + "sequence": "Sequence of the event", + "event_type": "Type of the event", + "created_at": "Time the event was created", + "userID": "ID of the creator of the event", + "event_payload": "Content of the event in JSON format" +} +``` + +## Target + +The Target describes how ZITADEL interacts with the Endpoint. + +There are different types of Targets: + +- `Webhook`, the call handles the status code but response is irrelevant, can be InterruptOnError +- `Call`, the call handles the status code and response, can be InterruptOnError +- `Async`, the call handles neither status code nor response, but can be called in parallel with other Targets + +`InterruptOnError` means that the Execution gets interrupted if any of the calls return with a status code >= 400, and the next Target will not be called anymore. + +The API documentation to create a target can be found [here](/apis/resources/action_service_v2/action-service-create-target) + +### Content Signing + +To ensure the integrity of request content, each call includes a 'ZITADEL-Signature' in the headers. This header contains an HMAC value computed from the request content and a timestamp, which can be used to time out requests. The logic for this process is provided in 'pkg/actions/signing.go'. The goal is to verify that the HMAC value in the header matches the HMAC value computed by the Target, ensuring that the sent and received requests are identical. + +Each Target resource now contains also a Signing Key, which gets generated and returned when a Target is [created](/apis/resources/action_service_v2/action-service-create-target), +and can also be newly generated when a Target is [patched](/apis/resources/action_service_v2/action-service-patch-target). + +For an example on how to check the signature, [refer to the example](/guides/integrate/actions/testing-request-signature). + +## Execution + +ZITADEL decides on specific conditions if one or more Targets have to be called. +The Execution resource contains 2 parts, the condition and the called targets. + +The condition can be defined for 4 types of processes: + +- `Requests`, before a request is processed by ZITADEL +- `Responses`, before a response is sent back to the application +- `Functions`, handling specific functionality in the logic of ZITADEL +- `Events`, after a specific event happened and was stored in ZITADEL + +The API documentation to set an Execution can be found [here](/apis/resources/action_service_v2/action-service-set-execution) + +### Condition Best Match + +As the conditions can be defined on different levels, ZITADEL tries to find out which Execution is the best match. +This means that for example if you have an Execution defined on `all requests`, on the service `zitadel.user.v2.UserService` and on `/zitadel.user.v2.UserService/AddHumanUser`, +ZITADEL would with a call on the `/zitadel.user.v2.UserService/AddHumanUser` use the Executions with the following priority: + +1. `/zitadel.user.v2.UserService/AddHumanUser` +2. `zitadel.user.v2.UserService` +3. `all` + +If you then have a call on `/zitadel.user.v2.UserService/UpdateHumanUser` the following priority would be found: + +1. `zitadel.user.v2.UserService` +2. `all` + +And if you use a different service, for example `zitadel.session.v2.SessionService`, then the `all` Execution would still be used. + +### Targets and Includes + +:::info +Includes are limited to 3 levels, which mean that include1->include2->include3 is the maximum for now. +If you have feedback to the include logic, or a reason why 3 levels are not enough, please open [an issue on github](https://github.com/zitadel/zitadel/issues) or [start a discussion on github](https://github.com/zitadel/zitadel/discussions)/[start a topic on discord](https://zitadel.com/chat) +::: + +An execution can not only contain a list of Targets, but also Includes. +The Includes can be defined in the Execution directly, which means you include all defined Targets by a before set Execution. + +If you define 2 Executions as follows: + +```json +{ + "condition": { + "request": { + "service": "zitadel.user.v2.UserService" + } + }, + "targets": [ + { + "target": "" + } + ] +} +``` + +```json +{ + "condition": { + "request": { + "method": "/zitadel.user.v2.UserService/AddHumanUser" + } + }, + "targets": [ + { + "target": "" + }, + { + "include": { + "request": { + "service": "zitadel.user.v2.UserService" + } + } + } + ] +} +``` + +The called Targets on "/zitadel.user.v2.UserService/AddHumanUser" would be, in order: + +1. `` +2. `` + +### Condition for Requests and Responses + +For Request and Response there are 3 levels the condition can be defined: + +- `Method`, handling a request or response of a specific GRPC full method, which includes the service name and method of the ZITADEL API +- `Service`, handling any request or response under a service of the ZITADEL API +- `All`, handling any request or response under the ZITADEL API + +The available conditions can be found under: +- [All available Methods](/apis/resources/action_service_v2/action-service-list-execution-methods), for example `/zitadel.user.v2.UserService/AddHumanUser` +- [All available Services](/apis/resources/action_service_v2/action-service-list-execution-services), for example `zitadel.user.v2.UserService` + +### Condition for Functions + +The available conditions can be found under [all available Functions](/apis/resources/action_service_v2/action-service-list-execution-functions). + +### Condition for Events + +For event there are 3 levels the condition can be defined: + +- Event, handling a specific event +- Group, handling a specific group of events +- All, handling any event in ZITADEL + +The concept of events can be found under [Events](/concepts/architecture/software#events) + +### Error forwarding + +If you want to forward a specific error from the Target through ZITADEL, you can provide a response from the Target with status code 200 and a JSON in the following format: + +```json +{ + "forwardedStatusCode": 403, + "forwardedErrorMessage": "Call is forbidden through the IP AllowList definition" +} +``` + +Only values from 400 to 499 will be forwarded through ZITADEL, other StatusCodes will end in a PreconditionFailed error. + +If the Target returns any other status code than >= 200 and < 299, the execution is looked at as failed, and a PreconditionFailed error is logged. diff --git a/docs/docs/guides/integrate/external-audit-log.md b/docs/docs/guides/integrate/external-audit-log.md index 93262d26f7..80dd41410d 100644 --- a/docs/docs/guides/integrate/external-audit-log.md +++ b/docs/docs/guides/integrate/external-audit-log.md @@ -20,7 +20,6 @@ The following table shows the available integration patterns for streaming audit | | Description | Self-hosting | ZITADEL Cloud | |-------------------------------------|----------------------------------------------------------------------------------------------------------------|-------------|---------------| | Events-API | Pulling events of all ZITADEL resources such as Users, Projects, Apps, etc. (Events = Change Log of Resources) | ✅ | ✅ | -| Cockroach Change Data Capture | Sending events of all ZITADEL resources such as Users, Projects, Apps, etc. (Events = Change Log of Resources) | ✅ | ❌ | | ZITADEL Actions Log to Stdout | Custom log to messages possible on predefined triggers during login / register Flow | ✅ | ❌ | | ZITADEL Actions trigger API/Webhook | Custom API/Webhook request on predefined triggers during login / register | ✅ | ✅ | @@ -34,71 +33,6 @@ This API offers granular control through various filters, enabling you to: You can find a comprehensive guide on how to use the events API for different use cases here: [Get Events from ZITADEL](/docs/guides/integrate/zitadel-apis/event-api) -### Cockroach Change Data Capture - -For self-hosted ZITADEL deployments utilizing CockroachDB as the database, [CockroachDB's built-in Change Data Capture (CDC)](https://www.cockroachlabs.com/docs/stable/change-data-capture-overview) functionality provides a streamlined approach to integrate ZITADEL audit logs with external systems. - -CDC captures row-level changes in your database and streams them as messages to a configurable destination, such as Google BigQuery or a SIEM/SOC solution. This real-time data stream enables: -- **Continuous monitoring**: Receive near-instantaneous updates on ZITADEL activity, facilitating proactive threat detection and response. -- **Simplified integration**: Leverage CockroachDB's native capabilities for real-time data transfer, eliminating the need for additional tools or configurations. - -This approach is limited to self-hosted deployments using CockroachDB and requires expertise in managing the database and CDC configuration. - -#### Sending events to Google Cloud Storage using Change Data Capture - -This example will show you how you can utilize CDC for sending all ZITADEL events to Google Cloud Storage. -For a detailed description please read [CockroachLab's Get Started Guide](https://www.cockroachlabs.com/docs/v23.2/create-and-configure-changefeeds) and [Cloud Storage Authentication](https://www.cockroachlabs.com/docs/v23.2/cloud-storage-authentication?filters=gcs#set-up-google-cloud-storage-assume-role) from Cockroach. - -You will need a Google Cloud Storage Bucket and a service account. -1. [Create Google Cloud Storage Bucket](https://cloud.google.com/storage/docs/creating-buckets) -2. [Create Service Account](https://cloud.google.com/iam/docs/service-accounts-create) -3. Create a role with the `storage.objects.create` permission -4. Grant service account access to the bucket -5. Create key for service account and download it - -Now we need to enable and create the changefeed in the cockroach DB. -1. [Enable rangefeeds on cockroach cluster](https://www.cockroachlabs.com/docs/v23.2/create-and-configure-changefeeds#enable-rangefeeds) - ```bash - SET CLUSTER SETTING kv.rangefeed.enabled = true; - ``` -2. Encode the keyfile from the service account with base64 and replace the placeholder it in the script below -3. Create Changefeed to send data into Google Cloud Storage - The following example sends all events without payload to Google Cloud Storage - Per default we do not want to send the payload of the events, as this could potentially include personally identifiable information (PII) - If you want to include the payload, you can just add `payload` to the select list in the query. - ```sql - CREATE CHANGEFEED INTO 'gs://gc-storage-zitadel-data/events?partition_format=flat&AUTH=specified&CREDENTIALS=base64encodedkey' - AS SELECT instance_id, aggregate_type, aggregate_id, owner, event_type, sequence, created_at - FROM eventstore.events2; - ``` - -In some cases you might want the payload of only some specific events. -This example shows you how to get all events and the instance domain events with the payload: - ```sql - CREATE CHANGEFEED INTO 'gs://gc-storage-zitadel-data/events?partition_format=flat&AUTH=specified&CREDENTIALS=base64encodedkey' - AS SELECT instance_id, aggregate_type, aggregate_id, owner, event_type, sequence, created_at - CASE WHEN event_type IN ('instance.domain.added', 'instance.domain.removed', 'instance.domain.primary.set' ) - THEN payload END AS payload - FROM eventstore.events2; - ``` - -The partition format in the example above is flat, this means that all files for each timestamp will be created in the same folder. -You will have files for different timestamps including the output for the events created in that time. -Each event is represented as a json row. - -Example Output: -```json lines -{ - "aggregate_id": "26553987123463875", - "aggregate_type": "user", - "created_at": "2023-12-25T10:01:45.600913Z", - "event_type": "user.human.added", - "instance_id": "123456789012345667", - "payload": null, - "sequence": 1 -} -``` - ## ZITADEL Actions ZITADEL [Actions](/docs/concepts/features/actions) offer a powerful mechanism for extending the platform's capabilities and integrating with external systems tailored to your specific requirements. diff --git a/docs/docs/guides/integrate/login/hosted-login.mdx b/docs/docs/guides/integrate/login/hosted-login.mdx index 4b9f3dda49..09fb86f8f0 100644 --- a/docs/docs/guides/integrate/login/hosted-login.mdx +++ b/docs/docs/guides/integrate/login/hosted-login.mdx @@ -138,6 +138,10 @@ In this initial release, the new login is available for self-hosting only. We'll ### Current State +:::info +The documentation describes the state of the feature in ZITADEL V3. +::: + Our primary goal for the TypeScript login system is to replace the existing login functionality within Zitadel Core, which is shipped with Zitadel automatically. This will allow us to leverage the benefits of the new system, including its modular architecture and enhanced security features. To achieve this, we are actively working on implementing the core features currently available in Zitadel Core, such as: @@ -160,15 +164,10 @@ As we continue to develop the TypeScript login system, we will provide regular u For the first implementation we have excluded the following features: -- SAML (SP & OP) - Generic JWT IDP - LDAP IDP - Device Authorization Grants -- Timebased features - - Lockout Settings - - Password Expiry Settings - - Login Settings - Multifactor init prompt - - Force MFA on external authenticated users +- Force MFA on external authenticated users - Passkey/U2F Setup - As passkey and u2f is bound to a domain, it is important to notice, that setting up the authentication possibility in the ZITADEL management console (Self-service), will not work if the login runs on a different domain - Custom Login Texts @@ -193,9 +192,9 @@ Your contributions will play a crucial role in shaping the future of our login s To activate it to authenticate on your Zitadel Cloud apps, you can follow one of these steps: - 1. Enable the new login on your application configuration and point it to `/ui/v2/login`. With these settings, Zitadel will automatically redirect you to the new login if you call the old one. + 1. Enable the new login on your application configuration. Leave the field **Custom base URL for the new Login UI** empty to use the default. With these settings, Zitadel will automatically redirect you to the new login if you call the old one. ![Login V2 Application Configuration](/img/guides/integrate/login/login-v2-app-config.png) - 2. Enable the [loginV2 feature](https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features) on the instance and add the base URI `/ui/v2/login`. If you enable this feature, the login will be used for every application configured in your Zitadel instance. (Example: https://your-zitadel-instance.zitadel.cloud/ui/v2/login) + 2. Enable the [loginV2 feature](https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features) on the instance. Leave the base URI empty to use the default. If you enable this feature, the login will be used for every application configured in your Zitadel instance. (Example: https://your-zitadel-instance.zitadel.cloud/ui/v2/login) diff --git a/docs/docs/guides/integrate/login/oidc/webkeys.md b/docs/docs/guides/integrate/login/oidc/webkeys.md index 2b414ae7e9..a66cae61a9 100644 --- a/docs/docs/guides/integrate/login/oidc/webkeys.md +++ b/docs/docs/guides/integrate/login/oidc/webkeys.md @@ -23,6 +23,7 @@ endpoints are called with a JWT access token. :::info Web keys are an [experimental](/docs/support/software-release-cycles-support#beta) feature. Be sure to enable the `web_key` [feature](/docs/apis/resources/feature_service_v2/feature-service-set-instance-features) before using it. +The documentation describes the state of the feature in ZITADEL V3. Test the feature and add improvement or bug reports directly to the [github repository](https://github.com/zitadel/zitadel) or let us know your general feedback in the [discord thread](https://discord.com/channels/927474939156643850/1329100936127320175/threads/1332344892629717075)! ::: @@ -112,7 +113,7 @@ When the request does not contain any specific configuration, [RSA](#rsa) is used as default with the default options as described below: ```bash -curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \ +curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -136,7 +137,7 @@ The RSA generator config takes two enum values. For example, to create a RSA web key with the size of 3072 bits and the SHA512 algorithm (RS512): ```bash -curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \ +curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -160,7 +161,7 @@ The ECDSA generator config takes a single `curve` enum value which determines bo For example, to create a ECDSA web key with a P-256 curve and the SHA256 algorithm: ```bash -curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \ +curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -184,7 +185,7 @@ Clients which support both curves must inspect `crv` header value to assert the For example, to create a ed25519 web key: ```bash -curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \ +curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -246,11 +247,11 @@ For the sake of this example we will use simplified IDs and restrict timestamps After one month, on 2025-02-01, we wish to activate the next available key and create a new key to be available for activation next month. This fulfills requirements 1 and 2. ```bash -curl -L -X POST 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys/2/_activate' \ +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/web_keys/2/_activate' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' -curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \ +curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -282,7 +283,7 @@ In addition to the activate and create calls we made on this iteration, we can now safely delete the oldest key, as both requirement 3 and 4 are now fulfilled: ```bash -curl -L -X DELETE 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys/1' \ +curl -L -X DELETE 'https://$CUSTOM-DOMAIN/v2beta/web_keys/1' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' ``` diff --git a/docs/docs/guides/integrate/zitadel-apis/access-zitadel-system-api.md b/docs/docs/guides/integrate/zitadel-apis/access-zitadel-system-api.md index 4e2b0b9973..4b96e9bbd5 100644 --- a/docs/docs/guides/integrate/zitadel-apis/access-zitadel-system-api.md +++ b/docs/docs/guides/integrate/zitadel-apis/access-zitadel-system-api.md @@ -16,6 +16,8 @@ To authenticate the user a self-signed JWT will be created and utilized. You can define any id for your user. This guide will assume it's `system-user-1`. +**NOTE:** system user id cannot contain capital letters + ## Generate an RSA keypair Generate an RSA private key with 2048 bit modulus: diff --git a/docs/docs/guides/manage/cloud/billing.md b/docs/docs/guides/manage/cloud/billing.md index 561986530c..0804d7a943 100644 --- a/docs/docs/guides/manage/cloud/billing.md +++ b/docs/docs/guides/manage/cloud/billing.md @@ -1,14 +1,7 @@ --- -title: Settings / Billing +title: Billing --- -## General - -In the general settings you can change your team name, notification settings and delete your team. - -![Customer Portal Settings General](/img/manuals/portal/customer_portal_settings_general.png) - - ## Billing In the billing page shows your configured payment methods and the invoice diff --git a/docs/docs/guides/manage/cloud/instances.md b/docs/docs/guides/manage/cloud/instances.md index 7bb3f16640..9fbc740521 100644 --- a/docs/docs/guides/manage/cloud/instances.md +++ b/docs/docs/guides/manage/cloud/instances.md @@ -54,9 +54,8 @@ To upgrade you must enter your billing information. If you hit a limit from the free tier you will automatically be asked to add your credit card information and to subscribe to the pro tier. You can also upgrade manually at any time. -1. Go to the settings tab -2. You can now see your Plan: "FREE" -3. Click "Upgrade" +1. Click the "Upgrade to PRO" button in the menu or go to the billing menu +2. If you choose the billing menu, you can now see your Free plan, click "Upgrade to Pro" 4. Add the missing data - Payment method: Credit Card Information - Customer: At least you have to fill the country @@ -70,7 +69,7 @@ We recommend register a custom domain to access your ZITADEL instance. The primary custom domain of your ZITADEL instance will be the issuer of the instance. All other custom domains can be used to access the instance itself 1. Browse to the "Custom Domains" Tab -2. Click **Add** +2. Click **Add domain** 3. Enter the domain you want and select the instance where the domain should belong to 4. In the next screen you will get all the information you will have to add to your DNS provider to verify your domain diff --git a/docs/docs/guides/manage/cloud/settings.md b/docs/docs/guides/manage/cloud/settings.md index 5b646db3e1..fe0ee9e752 100644 --- a/docs/docs/guides/manage/cloud/settings.md +++ b/docs/docs/guides/manage/cloud/settings.md @@ -2,7 +2,10 @@ title: Settings --- -Manage your team, email subscriptions, and billing information on the [Settings](https://zitadel.com/admin/settings) page. + +In the settings you can change your team name, notification settings and delete your team. + +![Customer Portal Settings General](/img/manuals/portal/customer_portal_settings_general.png) ## Team name diff --git a/docs/docs/guides/manage/console/_create-user.mdx b/docs/docs/guides/manage/console/_create-user.mdx index ce9141af72..3434284de2 100644 --- a/docs/docs/guides/manage/console/_create-user.mdx +++ b/docs/docs/guides/manage/console/_create-user.mdx @@ -3,9 +3,34 @@ To create a new user, go to Users and click on **New**. Enter the required conta import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; +:::note +If you started with Zitadel before version 3, you might have the "Human User [deprecated]" UI. +In this case please enable the Feature Flag "Use V2 Api in Console for User creation" in the Default Settings. +::: + + Invite Human + + When creating a new user you have different options. + First add the email, and select if the email address should be added automatically as "verified". + + In the last section you can choose the authentication options: + - **Setup authentication later for this user**: This flow might be useful if an employee starts at a later point but you already want to prepare the account. The user will not have an authentication method, before they will be able to login, they need to setup a method. + - **Send an invitation E-Mail for authentication setup and E-Mail verification**: The user will receive an email and be able to setup an authentication method (e.g Password, Passkey, External SSO). + - When using the [Zitadel Login V1](/docs/guides/integrate/login/hosted-login) the user will be prompted to setup a password + - When using the [Zitadel Login V2](/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta) the user has the option to choose the authentication method (password, passkey, identity provider), based on the configuration of the organization + Invite Human - Setup authentication method + - **Set an initial password for the user**: The user will receive an email and be able to setup an authentication method (e.g Password, Passkey, External SSO) + + + Add Human + + After a human user is created, by default, an initialization mail with a code is sent to the registered email. This code then has to be verified on first login. + If you want to omit this mail, you can check the **email verified** and **set initial password** toggle. + If no password is set initially, the initialization mail prompting the user to set his password is sent. + -After a human user is created, by default, an initialization mail with a code is sent to the registered email. This code then has to be verified on first login. -If you want to omit this mail, you can check the **email verified** and **set initial password** toggle. -If no password is set initially, the initialization mail prompting the user to set his password is sent. - You can prompt the user to add a second factor method too by checking the **Force MFA** toggle in [Login behaviour settings](/docs/guides/manage/console/default-settings#login-behavior-and-access). When logged in, a user can then manage the profile in the console, adding a profile picture, external IDPs and Passwordless authentication devices. diff --git a/docs/docs/guides/manage/console/default-settings.mdx b/docs/docs/guides/manage/console/default-settings.mdx index f21b650c6a..e8e36956a1 100644 --- a/docs/docs/guides/manage/console/default-settings.mdx +++ b/docs/docs/guides/manage/console/default-settings.mdx @@ -5,7 +5,7 @@ sidebar_label: Default Settings Default settings work as default or fallback settings for your organizational settings. Most of the time you only have to set default settings for the cases where you don't need specific behavior in the organizations themselves or you only have one organization. -To access default settings, use the settomgs page at `{instanceDomain}/ui/console/settings` or click at the default settings button on the **top-right** of the page and then navigate to settings in the navigation. +To access default settings, use the settings page at `{instanceDomain}/ui/console/settings` or click at the default settings button on the **top-right** of the page and then navigate to settings in the navigation. zitadel.zitadel.default.127.0.0.1.sslip.io -* password: Password1! +Open your browser at http://zitadel.default.127.0.0.1.sslip.io/ui/console?login_hint=zitadel-admin@zitadel.zitadel.default.127.0.0.1.sslip.io and use the initial password _Password1!_ -## VideoGuide - diff --git a/docs/docs/self-hosting/deploy/linux.mdx b/docs/docs/self-hosting/deploy/linux.mdx index eb7f4dc90d..90774e97ab 100644 --- a/docs/docs/self-hosting/deploy/linux.mdx +++ b/docs/docs/self-hosting/deploy/linux.mdx @@ -1,5 +1,5 @@ --- -title: Install ZITADEL on Linux +title: Install Zitadel on Linux sidebar_label: Linux --- @@ -11,7 +11,7 @@ import NoteInstanceNotFound from "./troubleshooting/_note_instance_not_found.mdx ## Install PostgreSQL Download a `postgresql` binary as described [in the PostgreSQL docs](https://www.postgresql.org/download/linux/). -ZITADEL is tested against PostgreSQL and CockroachDB latest stable tag and Ubuntu 22.04. +Zitadel is tested against PostgreSQL latest stable tag and latest Ubuntu LTS. ## Run PostgreSQL @@ -20,15 +20,15 @@ sudo systemctl start postgresql sudo systemctl enable postgresql ``` -## Install ZITADEL +## Install Zitadel -Download the ZITADEL release according to your architecture from [Github](https://github.com/zitadel/zitadel/releases/latest), unpack the archive and copy zitadel binary to /usr/local/bin +Download the Zitadel release according to your architecture from [Github](https://github.com/zitadel/zitadel/releases/latest), unpack the archive and copy zitadel binary to /usr/local/bin ```bash LATEST=$(curl -i https://github.com/zitadel/zitadel/releases/latest | grep location: | cut -d '/' -f 8 | tr -d '\r'); ARCH=$(uname -m); case $ARCH in armv5*) ARCH="armv5";; armv6*) ARCH="armv6";; armv7*) ARCH="arm";; aarch64) ARCH="arm64";; x86) ARCH="386";; x86_64) ARCH="amd64";; i686) ARCH="386";; i386) ARCH="386";; esac; wget -c https://github.com/zitadel/zitadel/releases/download/$LATEST/zitadel-linux-$ARCH.tar.gz -O - | tar -xz && sudo mv zitadel-linux-$ARCH/zitadel /usr/local/bin ``` -## Run ZITADEL +## Run Zitadel ```bash ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=root ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=postgres ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable ZITADEL_EXTERNALSECURE=false zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled @@ -50,7 +50,7 @@ ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZIT allowfullscreen > -### Setup ZITADEL with a service account +### Setup Zitadel with a service account ```bash ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=root ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable ZITADEL_EXTERNALSECURE=false ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH=/tmp/zitadel-admin-sa.json ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_TYPE=1 zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml index 94d8f438dc..d1d8c95bb2 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml @@ -25,7 +25,7 @@ services: - './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro' db: - image: postgres:16-alpine + image: postgres:17-alpine restart: always environment: - POSTGRES_USER=root diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx index 7f25a9b210..d5e3984568 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx @@ -81,5 +81,5 @@ Read more about [the login process](/guides/integrate/login/oidc/login-users). ## Troubleshooting -You can connect to cockroach like this: `docker exec -it loadbalancing-example-my-cockroach-db-1 cockroach sql --host my-cockroach-db --certs-dir /cockroach/certs/` -For example, to show all login names: `docker exec -it loadbalancing-example-my-cockroach-db-1 cockroach sql --database zitadel --host my-cockroach-db --certs-dir /cockroach/certs/ --execute "select * from projections.login_names3"` +You can connect to the database like this: `docker exec -it loadbalancing-example-db-1 psql --host localhost` +For example, to show all login names: `docker exec -it loadbalancing-example-db-1 psql -d zitadel --host localhost -c 'select * from projections.login_names3'` diff --git a/docs/docs/self-hosting/deploy/macos.mdx b/docs/docs/self-hosting/deploy/macos.mdx index f736255478..beb3182208 100644 --- a/docs/docs/self-hosting/deploy/macos.mdx +++ b/docs/docs/self-hosting/deploy/macos.mdx @@ -11,7 +11,7 @@ import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx ## Install PostgreSQL Download a `postgresql` binary as described [in the PostgreSQL docs](https://www.postgresql.org/download/macosx/). -ZITADEL is tested against PostgreSQL and CockroachDB latest stable tag and Ubuntu 22.04. +ZITADEL is tested against PostgreSQL latest stable tag and latest Ubuntu LTS. ## Run PostgreSQL diff --git a/docs/docs/self-hosting/deploy/overview.mdx b/docs/docs/self-hosting/deploy/overview.mdx index 38517c52f4..68255d4ce3 100644 --- a/docs/docs/self-hosting/deploy/overview.mdx +++ b/docs/docs/self-hosting/deploy/overview.mdx @@ -14,7 +14,7 @@ Choose your platform and run ZITADEL with the most minimal configuration possibl ## Prerequisites - For test environments, ZITADEL does not need many resources, 1 CPU and 512MB memory are more than enough. (With more CPU, the password hashing might be faster) -- A PostgreSQL or CockroachDB as only needed storage. Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide to use Postgresql. +- A PostgreSQL as only needed storage. Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide to use Postgresql. ## Releases diff --git a/docs/docs/self-hosting/manage/cache.md b/docs/docs/self-hosting/manage/cache.md index 30619ad283..32973d5586 100644 --- a/docs/docs/self-hosting/manage/cache.md +++ b/docs/docs/self-hosting/manage/cache.md @@ -110,7 +110,6 @@ Drawbacks: - Slowest of the available caching options - Might put additional strain on the database server, limiting horizontal scalability -- CockroachDB does not support unlogged tables. When this connector is enabled against CockroachDB, it does work but little to no performance benefit is to be expected. ### Local memory cache diff --git a/docs/docs/self-hosting/manage/cli/mirror.mdx b/docs/docs/self-hosting/manage/cli/mirror.mdx index 1c32dc8741..ae81800e39 100644 --- a/docs/docs/self-hosting/manage/cli/mirror.mdx +++ b/docs/docs/self-hosting/manage/cli/mirror.mdx @@ -1,5 +1,5 @@ --- -title: Mirror data to another database +title: Mirror data from cockroach to postgres sidebar_label: Mirror command --- @@ -9,15 +9,15 @@ The data can be mirrored to multiple database without influencing each other. ## Use cases -Migrate from cockroachdb to postgres or vice versa. +Migrate from cockroachdb to postgres. Replicate data to a secondary environment for testing. ## Prerequisites -You need an existing source database, most probably the database ZITADEL currently serves traffic from. +You need an existing source database, most probably the database Zitadel currently serves traffic from. -To mirror the data the destination database needs to be initialized and setup without an instance. +To mirror the data, the destination database needs to be initialized and set up without an instance. You can find the commands to start an empty Zitadel deployment in [the example section](#prepare-the-destination-database). ### Start the destination database @@ -38,14 +38,32 @@ docker compose up db --detach ## Example -The following commands setup the database as described above. See [configuration](#configuration) for more details about the configuration options. +### Prepare the destination database + +The following commands setup the database without an instance. ```bash zitadel init --config /path/to/your/new/config.yaml zitadel setup --for-mirror --config /path/to/your/new/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment +``` + +### Mirror the data + +The next step is to copy the data from the source to the destination database. For detailed configuration options, please refer to the [configuration section](#configuration). + +```bash zitadel mirror --system --config /path/to/your/mirror/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment ``` +### Initialize the data and verify + +The last step is to setup the permissions and verify the data, there might be differences between source and destination, refer to [`zitadel mirror verify`](#zitadel-mirror-verify) to get an overview of possible diffs. + +```bash +zitadel setup --for-mirror --config /path/to/your/new/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment +zitadel mirror verify --system --config /path/to/your/mirror/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment +``` + ## Usage The general syntax for the mirror command is: @@ -73,7 +91,7 @@ Flags: --masterkey string masterkey as argument for en/decryption keys -m, --masterkeyFile string path to the masterkey for en/decryption keys --masterkeyFromEnv read masterkey for en/decryption keys from environment variable (ZITADEL_MASTERKEY) - --tlsMode externalSecure start ZITADEL with (enabled), without (disabled) TLS or external component e.g. reverse proxy (external) terminating TLS, this flag will overwrite externalSecure and `tls.enabled` in configs files + --tlsMode externalSecure start Zitadel with (enabled), without (disabled) TLS or external component e.g. reverse proxy (external) terminating TLS, this flag will overwrite externalSecure and `tls.enabled` in configs files ``` ## Configuration @@ -87,8 +105,6 @@ Source: Database: zitadel # ZITADEL_SOURCE_COCKROACH_DATABASE MaxOpenConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXOPENCONNS MaxIdleConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXIDLECONNS - EventPushConnRatio: 0.33 # ZITADEL_SOURCE_COCKROACH_EVENTPUSHCONNRATIO - ProjectionSpoolerConnRatio: 0.33 # ZITADEL_SOURCE_COCKROACH_PROJECTIONSPOOLERCONNRATIO MaxConnLifetime: 30m # ZITADEL_SOURCE_COCKROACH_MAXCONNLIFETIME MaxConnIdleTime: 5m # ZITADEL_SOURCE_COCKROACH_MAXCONNIDLETIME Options: "" # ZITADEL_SOURCE_COCKROACH_OPTIONS @@ -122,47 +138,29 @@ Source: # The destination database the data are copied to. Use either cockroach or postgres, by default cockroach is used Destination: - cockroach: - Host: localhost # ZITADEL_DESTINATION_COCKROACH_HOST - Port: 26257 # ZITADEL_DESTINATION_COCKROACH_PORT - Database: zitadel # ZITADEL_DESTINATION_COCKROACH_DATABASE - MaxOpenConns: 0 # ZITADEL_DESTINATION_COCKROACH_MAXOPENCONNS - MaxIdleConns: 0 # ZITADEL_DESTINATION_COCKROACH_MAXIDLECONNS - MaxConnLifetime: 30m # ZITADEL_DESTINATION_COCKROACH_MAXCONNLIFETIME - MaxConnIdleTime: 5m # ZITADEL_DESTINATION_COCKROACH_MAXCONNIDLETIME - EventPushConnRatio: 0.01 # ZITADEL_DESTINATION_COCKROACH_EVENTPUSHCONNRATIO - ProjectionSpoolerConnRatio: 0.5 # ZITADEL_DESTINATION_COCKROACH_PROJECTIONSPOOLERCONNRATIO - Options: "" # ZITADEL_DESTINATION_COCKROACH_OPTIONS - User: - Username: zitadel # ZITADEL_DESTINATION_COCKROACH_USER_USERNAME - Password: "" # ZITADEL_DESTINATION_COCKROACH_USER_PASSWORD - SSL: - Mode: disable # ZITADEL_DESTINATION_COCKROACH_USER_SSL_MODE - RootCert: "" # ZITADEL_DESTINATION_COCKROACH_USER_SSL_ROOTCERT - Cert: "" # ZITADEL_DESTINATION_COCKROACH_USER_SSL_CERT - Key: "" # ZITADEL_DESTINATION_COCKROACH_USER_SSL_KEY - # Postgres is used as soon as a value is set - # The values describe the possible fields to set values postgres: - Host: # ZITADEL_DESTINATION_POSTGRES_HOST - Port: # ZITADEL_DESTINATION_POSTGRES_PORT - Database: # ZITADEL_DESTINATION_POSTGRES_DATABASE - MaxOpenConns: # ZITADEL_DESTINATION_POSTGRES_MAXOPENCONNS - MaxIdleConns: # ZITADEL_DESTINATION_POSTGRES_MAXIDLECONNS - MaxConnLifetime: # ZITADEL_DESTINATION_POSTGRES_MAXCONNLIFETIME - MaxConnIdleTime: # ZITADEL_DESTINATION_POSTGRES_MAXCONNIDLETIME - Options: # ZITADEL_DESTINATION_POSTGRES_OPTIONS + Host: localhost # ZITADEL_DATABASE_POSTGRES_HOST + Port: 5432 # ZITADEL_DATABASE_POSTGRES_PORT + Database: zitadel # ZITADEL_DATABASE_POSTGRES_DATABASE + MaxOpenConns: 5 # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS + MaxIdleConns: 2 # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS + MaxConnLifetime: 30m # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME + MaxConnIdleTime: 5m # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME + Options: "" # ZITADEL_DATABASE_POSTGRES_OPTIONS User: - Username: # ZITADEL_DESTINATION_POSTGRES_USER_USERNAME - Password: # ZITADEL_DESTINATION_POSTGRES_USER_PASSWORD + Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME + Password: "" # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD SSL: - Mode: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_MODE - RootCert: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_ROOTCERT - Cert: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_CERT - Key: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_KEY + Mode: disable # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE + RootCert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT + Cert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT + Key: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY # As cockroachdb first copies the data into memory this parameter is used to iterate through the events table and fetch only the given amount of events per iteration EventBulkSize: 10000 # ZITADEL_EVENTBULKSIZE +# The maximum duration an auth request was last updated before it gets ignored. +# Default is 30 days +MaxAuthRequestAge: 720h # ZITADEL_MAXAUTHREQUESTAGE Projections: # Defines how many projections are allowed to run in parallel @@ -227,6 +225,6 @@ It is not possible to use files as source or destination. See github issue [here Currently the encryption keys of the source database must be copied to the destination database. See github issue [here](https://github.com/zitadel/zitadel/issues/7964) -It is not possible to change the domain of the ZITADEL deployment. +It is not possible to change the domain of the Zitadel deployment. Once you mirrored an instance using the `--instance` flag, you have to make sure you don't mirror other preexisting instances. This means for example, you cannot mirror a few instances and then pass the `--system` flag. You have to pass all remaining instances explicitly, once you used the `--instance` flag diff --git a/docs/docs/self-hosting/manage/configure/_helm.mdx b/docs/docs/self-hosting/manage/configure/_helm.mdx index 9f03e4237a..b35957abb8 100644 --- a/docs/docs/self-hosting/manage/configure/_helm.mdx +++ b/docs/docs/self-hosting/manage/configure/_helm.mdx @@ -1,36 +1,6 @@ -import CodeBlock from '@theme/CodeBlock'; -import ExampleZITADELValuesSource from '!!raw-loader!./example-zitadel-values.yaml' -import ExampleZITADELValuesSecretsSource from '!!raw-loader!./example-zitadel-values-secrets.yaml' - -By default, the chart installs a secure ZITADEL and CockroachDB. -The example files makes an insecure ZITADEL accessible by port forwarding the ZITADEL service to localhost. -For more configuration options, [go to the chart repo descriptions](https://github.com/zitadel/zitadel-charts). -For a secure installation with Docker Compose, [go to the loadbalancing example](/self-hosting/deploy/loadbalancing-example) - -By executing the commands below, you will download the following files: - -
- example-zitadel-values.yaml - {ExampleZITADELValuesSource} -
- -
- example-zitadel-values-secrets.yaml - {ExampleZITADELValuesSecretsSource} -
- -```bash -# Download and adjust the example configuration file containing standard configuration -wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/configure/example-zitadel-values.yaml - -# Download and adjust the example configuration file containing secret configuration -wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/configure/example-zitadel-values-secrets.yaml - -# Install an insecure zitadel release that works with localhost -helm install --namespace zitadel --create-namespace my-zitadel zitadel/zitadel \ - --values ./example-zitadel-values.yaml \ - --values ./example-zitadel-values-secrets.yaml - -# Forward the ZITADEL service port to your local machine -kubectl --namespace zitadel port-forward svc/my-zitadel 8080:80 -``` +To run Zitadel on Kubernetes, use [the official Zitadel Helm chart](https://github.com/zitadel/zitadel-charts). +Configure Zitadel using native Helm values. +You can manage secrets through Helm values, letting Helm create Kubernetes secrets. +Alternatively, reference existing Kubernetes secrets managed outside of Helm. +See the [referenced secrets example](https://github.com/zitadel/zitadel-charts/tree/main/examples/3-referenced-secrets) in the charts */examples* folder. +For a quick setup, check out the [insecure Postgres example](https://github.com/zitadel/zitadel-charts/tree/main/examples/1-insecure-postgres). diff --git a/docs/docs/self-hosting/manage/configure/_login.md b/docs/docs/self-hosting/manage/configure/_login.md new file mode 100644 index 0000000000..2fc258b299 --- /dev/null +++ b/docs/docs/self-hosting/manage/configure/_login.md @@ -0,0 +1 @@ +Open your favorite internet browser at http://localhost:8080/ui/console?login_hint=root@zitadel.localhost and use the password _RootPassword1!_ diff --git a/docs/docs/self-hosting/manage/configure/configure.mdx b/docs/docs/self-hosting/manage/configure/configure.mdx index aaf221dfda..c68f716d63 100644 --- a/docs/docs/self-hosting/manage/configure/configure.mdx +++ b/docs/docs/self-hosting/manage/configure/configure.mdx @@ -8,6 +8,7 @@ import TabItem from "@theme/TabItem"; import LinuxUnix from "./_linuxunix.mdx"; import Compose from "./_compose.mdx"; import Helm from "./_helm.mdx"; +import Login from "./_login.md"; import CodeBlock from "@theme/CodeBlock"; import DefaultsYamlSource from "!!raw-loader!./defaults.yaml"; import StepsYamlSource from "!!raw-loader!./steps.yaml"; @@ -90,21 +91,17 @@ There are three ways to pass the masterkey to the `zitadel` binary: > + + -Open your favorite internet browser at [http://localhost:8080/ui/console](http://localhost:8080/ui/console). -This is the IAM admin users login according to your configuration in the [example-zitadel-init-steps.yaml](./example-zitadel-init-steps.yaml): - -- **username**: _root@zitadel.localhost_ -- **password**: _RootPassword1!_ - ## What's next - Read more about [the login process](/guides/integrate/login/login-users). diff --git a/docs/docs/self-hosting/manage/configure/docker-compose.yaml b/docs/docs/self-hosting/manage/configure/docker-compose.yaml index abd1818a7b..3fd0e5471c 100644 --- a/docs/docs/self-hosting/manage/configure/docker-compose.yaml +++ b/docs/docs/self-hosting/manage/configure/docker-compose.yaml @@ -11,9 +11,12 @@ services: - "./example-zitadel-config.yaml:/example-zitadel-config.yaml:ro" - "./example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro" - "./example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro" + depends_on: + db: + condition: "service_healthy" db: - image: postgres:16-alpine + image: postgres:17-alpine restart: always environment: - POSTGRES_USER=root @@ -25,7 +28,7 @@ services: interval: 10s timeout: 60s retries: 5 - start_period: 10s + start_period: 10s volumes: - 'data:/var/lib/postgresql/data:rw' @@ -34,3 +37,4 @@ networks: volumes: data: + diff --git a/docs/docs/self-hosting/manage/configure/example-zitadel-values-secrets.yaml b/docs/docs/self-hosting/manage/configure/example-zitadel-values-secrets.yaml deleted file mode 100644 index 99e5ce5647..0000000000 --- a/docs/docs/self-hosting/manage/configure/example-zitadel-values-secrets.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml -zitadel: - - masterkey: 'MasterkeyNeedsToHave32Characters' - - secretConfig: - - Database: - postgres: - User: - # If the user doesn't exist already, it is created - Username: 'root' - Password: 'Secret_DB_User_Password' - Admin: - Username: 'root' - Password: '' diff --git a/docs/docs/self-hosting/manage/configure/example-zitadel-values.yaml b/docs/docs/self-hosting/manage/configure/example-zitadel-values.yaml deleted file mode 100644 index 571c7af699..0000000000 --- a/docs/docs/self-hosting/manage/configure/example-zitadel-values.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml -zitadel: - configmapConfig: - Log: - Level: 'info' - - # Make ZITADEL accessible over HTTP, not HTTPS - ExternalSecure: false - ExternalDomain: localhost - - # the configmap is also passed to the zitadel binary via the --steps flag - FirstInstance: - Org: - Human: - # use the loginname root@zitadel.localhost - Username: 'root' - Password: 'RootPassword1!' diff --git a/docs/docs/self-hosting/manage/database/_cockroachdb.mdx b/docs/docs/self-hosting/manage/database/_cockroachdb.mdx index edc3f139fd..90c99470e6 100644 --- a/docs/docs/self-hosting/manage/database/_cockroachdb.mdx +++ b/docs/docs/self-hosting/manage/database/_cockroachdb.mdx @@ -1,8 +1,12 @@ -## ZITADEL with Cockroach +## Zitadel v2 with Cockroach -The default database of ZITADEL is [CockroachDB](https://www.cockroachlabs.com). The SQL database provides a bunch of features like horizontal scalability, data regionality and many more. +:::warning +Zitadel v3 removed CockroachDB support. See the [CLI mirror guide](../cli/mirror) for migrating to PostgreSQL. +::: -Currently versions >= 23.2 are supported. +The default database of Zitadel v2 is [CockroachDB](https://www.cockroachlabs.com). The SQL database provides a bunch of features like horizontal scalability, data regionality and many more. + +Currently versions >= 25.1 are supported. The default configuration of the database looks like this: diff --git a/docs/docs/self-hosting/manage/database/_postgres.mdx b/docs/docs/self-hosting/manage/database/_postgres.mdx index 604d6b39a5..719fb9469e 100644 --- a/docs/docs/self-hosting/manage/database/_postgres.mdx +++ b/docs/docs/self-hosting/manage/database/_postgres.mdx @@ -1,6 +1,8 @@ ## ZITADEL with Postgres -If you want to use a PostgreSQL database you can [overwrite the default configuration](../configure/configure.mdx). +PostgreSQL is the default database for ZITADEL due to its reliability, robustness, and adherence to SQL standards. It is well-suited for handling the complex data requirements of an identity management system. + +If you are using Zitadel v2 and want to use a PostgreSQL database you can [overwrite the default configuration](../configure/configure.mdx). Currently versions >= 14 are supported. diff --git a/docs/docs/self-hosting/manage/database/database.mdx b/docs/docs/self-hosting/manage/database/database.mdx index c67ecbaaba..df491e1565 100644 --- a/docs/docs/self-hosting/manage/database/database.mdx +++ b/docs/docs/self-hosting/manage/database/database.mdx @@ -11,10 +11,10 @@ import Postgres from './_postgres.mdx' diff --git a/docs/docs/self-hosting/manage/production.md b/docs/docs/self-hosting/manage/production.md index fde620b13e..98296281ea 100644 --- a/docs/docs/self-hosting/manage/production.md +++ b/docs/docs/self-hosting/manage/production.md @@ -111,14 +111,15 @@ but in the Projections.Customizations.Telemetry section ### Prefer PostgreSQL -ZITADEL supports [CockroachDB](https://www.cockroachlabs.com/) and [PostgreSQL](https://www.postgresql.org/). -We recommend using PostgreSQL, as it is the better choice when you want to prioritize performance and latency. +ZITADEL supports [PostgreSQL](https://www.postgresql.org/). -However, if [multi-regional data locality](https://www.cockroachlabs.com/docs/stable/multiregion-overview.html) is a critical requirement, CockroachDB might be a suitable option. +:::info +ZITADEL v2 supports [CockroachDB](https://www.cockroachlabs.com/) and [PostgreSQL](https://www.postgresql.org/). Please refer to [the mirror guide](cli/mirror) to migrate to postgres. +::: The indexes for the database are optimized using load tests from [ZITADEL Cloud](https://zitadel.com), which runs with PostgreSQL. -If you identify problems with your CockroachDB during load tests that indicate that the indexes are not optimized, +If you identify problems with your database during load tests that indicate that the indexes are not optimized, please create an issue in our [github repository](https://github.com/zitadel/zitadel). ### Configure ZITADEL @@ -129,12 +130,13 @@ Depending on your environment, you maybe would want to tweak some settings about Database: postgres: Host: localhost - Port: 26257 + Port: 5432 Database: zitadel //highlight-start - MaxOpenConns: 20 + MaxOpenConns: 10 + MaxIdleConns: 5 MaxConnLifetime: 30m - MaxConnIdleTime: 30m + MaxConnIdleTime: 5m //highlight-end Options: "" ``` @@ -192,9 +194,7 @@ The ZITADEL binary itself is stateless, so there is no need for a special backup job. Generally, for maintaining your database management system in production, -please refer to the corresponding docs -[for CockroachDB](https://www.cockroachlabs.com/docs/stable/recommended-production-settings.html) -or [for PostgreSQL](https://www.postgresql.org/docs/current/admin.html). +please refer to the corresponding docs [for PostgreSQL](https://www.postgresql.org/docs/current/admin.html). ## Data initialization @@ -240,8 +240,7 @@ you might want to [limit usage and/or execute tasks on certain usage units and l ### General resource usage -ZITADEL consumes around 512MB RAM and can run with less than 1 CPU core. -The database consumes around 2 CPU under normal conditions and 6GB RAM with some caching to it. +ZITADEL itself requires approximately 512MB of RAM and can operate with less than one CPU core. The database component, under typical conditions, utilizes about one CPU core per 100 requests per second (req/s) and 4GB of RAM per core, which includes some caching. :::info Password hashing Be aware of CPU spikes when hashing passwords. We recommend to have 4 CPU cores available for this purpose. @@ -249,5 +248,6 @@ Be aware of CPU spikes when hashing passwords. We recommend to have 4 CPU cores ### Production HA cluster -It is recommended to build a minimal high-availability with 3 Nodes with 4 CPU and 16GB memory each. -Excluding non-essential services, such as log collection, metrics etc, the resources could be reduced to around 4 CPU and 8GB memory each. +For a minimal high-availability setup, we recommend a cluster of 3 nodes, each with 4 CPU cores and 16GB of memory. + +If you exclude non-essential services like log collection and metrics, you can reduce the resources to approximately 4 CPU cores and 8GB of memory per node. diff --git a/docs/docs/self-hosting/manage/productionchecklist.md b/docs/docs/self-hosting/manage/productionchecklist.md index fb85557a23..c4f6491d82 100644 --- a/docs/docs/self-hosting/manage/productionchecklist.md +++ b/docs/docs/self-hosting/manage/productionchecklist.md @@ -19,7 +19,9 @@ To apply best practices to your production setup we created a step by step check - [ ] Use serverless platform such as Knative or a hyperscaler equivalent (e.g. CloudRun from Google) - [ ] Split `zitadel init` and `zitadel setup` for fast start-up times when [scaling](/docs/self-hosting/manage/updating_scaling) ZITADEL - [ ] High Availability for database - - [ ] Follow the [Production Checklist](https://www.cockroachlabs.com/docs/stable/recommended-production-settings.html) for CockroachDB if you selfhost the database or use [CockroachDB cloud](https://www.cockroachlabs.com/docs/cockroachcloud/create-an-account.html) + - [ ] Follow [this guide](https://www.postgresql.org/docs/current/high-availability.html) to set up the database. + - [ ] Configure logging + - [ ] Configure timeouts - [ ] Configure backups on a regular basis for the database - [ ] Test the restore scenarios before going live - [ ] Secure database connections from outside your network and/or use an internal subnet for database connectivity diff --git a/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_overview.mdx b/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_overview.mdx index 3200ddf379..f72eb6eec5 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_overview.mdx +++ b/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_overview.mdx @@ -2,13 +2,13 @@ import CodeBlock from '@theme/CodeBlock'; import ComposeYaml from "!!raw-loader!./docker-compose.yaml"; <>With these examples, you create and run a minimal {props.link} configuration for ZITADEL with Docker Compose. -Whereas the guide focuses on the configuration for {props.link}, you can inspect the configurations for ZITADEL and the database in the base Docker Compose file. +Whereas the guide focuses on the configuration for {props.name}, you can inspect the configurations for ZITADEL and the database in the base Docker Compose file.
base docker-compose.yaml {ComposeYaml}
-<>For running {props.link}, you will extend the base Docker Compose file with the {props.link} specific Docker Compose file. +<>For running {props.name}, you will extend the base Docker Compose file with the {props.name} specific Docker Compose file.
specific docker-compose.yaml diff --git a/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_tls_mode.mdx b/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_tls_mode.mdx index 1cacf076e5..43663af486 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_tls_mode.mdx +++ b/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_tls_mode.mdx @@ -1,25 +1,25 @@ import CodeBlock from '@theme/CodeBlock'; -export const Description = ({mode, link}) => { +export const Description = ({mode, name}) => { let desc switch (mode) { case "disabled": - desc = <>Neither {link} nor ZITADEL terminates TLS. - Nevertheless, {link} forwards unencrypted HTTP/2 traffic, aka h2c, to ZITADEL.; + desc = <>Neither {name} nor ZITADEL terminates TLS. + Nevertheless, {name} forwards unencrypted HTTP/2 traffic, aka h2c, to ZITADEL.; break; case "external": - desc = <>{link} terminates TLS and forwards the requests to ZITADEL via unencrypted h2c. - This example uses an unsafe self-signed certificate for {link}; + desc = <>{name} terminates TLS and forwards the requests to ZITADEL via unencrypted h2c. + This example uses an unsafe self-signed certificate for {name}; break; case "enabled": - desc = <>{link} terminates TLS and forwards the requests to ZITADEL via encrypted HTTP/2. - This example uses an unsafe self-signed certificate for {link} and the same for ZITADEL.; + desc = <>{name} terminates TLS and forwards the requests to ZITADEL via encrypted HTTP/2. + This example uses an unsafe self-signed certificate for {name} and the same for ZITADEL.; break; } return ( <> {desc} - <>By executing the commands below, you will download the files necessary to run ZITADEL behind {link} with the following config: + <>By executing the commands below, you will download the files necessary to run ZITADEL behind {name} with the following config: ) } diff --git a/docs/docs/self-hosting/manage/reverseproxy/caddy/caddy.mdx b/docs/docs/self-hosting/manage/reverseproxy/caddy/caddy.mdx index 20a00dae9b..5fb9ea4014 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/caddy/caddy.mdx +++ b/docs/docs/self-hosting/manage/reverseproxy/caddy/caddy.mdx @@ -15,7 +15,7 @@ export const providername = 'Caddy'; export const lower = "caddy"; export const link = {providername} - + You can either setup your environment for TLS mode external or TLS mode enabled. diff --git a/docs/docs/self-hosting/manage/reverseproxy/docker-compose.yaml b/docs/docs/self-hosting/manage/reverseproxy/docker-compose.yaml index 989b620fef..c4a4f93fb2 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/docker-compose.yaml +++ b/docs/docs/self-hosting/manage/reverseproxy/docker-compose.yaml @@ -121,7 +121,7 @@ services: db: restart: 'always' - image: postgres:16-alpine + image: postgres:17-alpine environment: POSTGRES_PASSWORD: postgres healthcheck: diff --git a/docs/docs/self-hosting/manage/reverseproxy/httpd/httpd.mdx b/docs/docs/self-hosting/manage/reverseproxy/httpd/httpd.mdx index 4d75802ec4..c869155d05 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/httpd/httpd.mdx +++ b/docs/docs/self-hosting/manage/reverseproxy/httpd/httpd.mdx @@ -15,7 +15,7 @@ export const providername = "Apache httpd"; export const lower = "httpd"; export const link = {providername} - + You can either setup your environment for TLS mode disabled, TLS mode external or TLS mode enabled. diff --git a/docs/docs/self-hosting/manage/reverseproxy/nginx/nginx.mdx b/docs/docs/self-hosting/manage/reverseproxy/nginx/nginx.mdx index 0ad5c036b7..fa3a9e75de 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/nginx/nginx.mdx +++ b/docs/docs/self-hosting/manage/reverseproxy/nginx/nginx.mdx @@ -15,7 +15,7 @@ export const providername = 'NGINX'; export const lower = "nginx"; export const link = {providername}; - + You can either setup your environment for TLS mode disabled, TLS mode external or TLS mode enabled. diff --git a/docs/docs/self-hosting/manage/reverseproxy/traefik/traefik.mdx b/docs/docs/self-hosting/manage/reverseproxy/traefik/traefik.mdx index d5950ad93c..39769b229b 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/traefik/traefik.mdx +++ b/docs/docs/self-hosting/manage/reverseproxy/traefik/traefik.mdx @@ -15,7 +15,7 @@ export const providername = 'Traefik'; export const lower = "traefik"; export const link = {providername}; - + You can either setup your environment for TLS mode disabled, TLS mode external or TLS mode enabled. diff --git a/docs/docs/self-hosting/manage/updating_scaling.md b/docs/docs/self-hosting/manage/updating_scaling.md index 7b8c72bd32..046c8891d0 100644 --- a/docs/docs/self-hosting/manage/updating_scaling.md +++ b/docs/docs/self-hosting/manage/updating_scaling.md @@ -53,10 +53,10 @@ The command `zitadel init` ensures the database connection is ready to use for t It just needs to be executed once over ZITADELs full life cycle, when you install ZITADEL from scratch. During `zitadel init`, for connecting to your database, -ZITADEL uses the privileged and preexisting database user configured in `Database..Admin.Username`. +ZITADEL uses the privileged and preexisting database user configured in `Database.postgres.Admin.Username`. , `zitadel init` ensures the following: - If it doesn’t exist already, it creates a database with the configured database name. -- If it doesn’t exist already, it creates the unprivileged user use configured in `Database..User.Username`. +- If it doesn’t exist already, it creates the unprivileged user use configured in `Database.postgres.User.Username`. Subsequent phases connect to the database with this user's credentials only. - If not already done, it grants the necessary permissions ZITADEL needs to the non privileged user. - If they don’t exist already, it creates all schemas and some basic tables. diff --git a/docs/docs/support/advisory/a10015.md b/docs/docs/support/advisory/a10015.md new file mode 100644 index 0000000000..0945f40361 --- /dev/null +++ b/docs/docs/support/advisory/a10015.md @@ -0,0 +1,21 @@ +--- +title: Technical Advisory 10015 +--- + +## Date + +Versions: >= v3.0.0 + +Date: 2025-03-31 + +## Description + +CockroachDB was initially chosen due to its distributed nature and SQL compatibility. However, over time, it became apparent that the operational complexity and specific compatibility issues outweighed the benefits for our use case. We decided to focus on PostgreSQL to simplify our infrastructure and leverage its mature ecosystem. + +## Impact + +Zitadel v3 requires PostgreSQL as a database. Therefore, Zitadel v3 will not start if CockroachDB is configured as the database. + +## Mitigation + +To upgrade your self-hosted deployment to Zitadel v3 migrate to PostgreSQL. Please refer to [this guide](/docs/self-hosting/manage/cli/mirror) to mirror the data to PostgreSQL before you deploy Zitadel v3. \ No newline at end of file diff --git a/docs/docs/support/technical_advisory.mdx b/docs/docs/support/technical_advisory.mdx index 8805e2e1d8..0d8818c32c 100644 --- a/docs/docs/support/technical_advisory.mdx +++ b/docs/docs/support/technical_advisory.mdx @@ -226,6 +226,18 @@ We understand that these advisories may include breaking changes, and we aim to
+ + + + + + + +
{{ 'USER.MFA.TABLETYPE' | translate }}- 2025-01-10
+ A-10015 + Drop CockroachDB supportBreaking Behavior Change + CockroachDB is no longer supported by Zitadel. + 3.0.02025-03-31
## Subscribe to our Mailing List diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index c10c89e127..1f45a017ac 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -324,17 +324,17 @@ module.exports = { categoryLinkSource: "auto", }, }, - action_v3: { - specPath: ".artifacts/openapi/zitadel/resources/action/v3alpha/action_service.swagger.json", - outputDir: "docs/apis/resources/action_service_v3", + action_v2: { + specPath: ".artifacts/openapi/zitadel/action/v2beta/action_service.swagger.json", + outputDir: "docs/apis/resources/action_service_v2", sidebarOptions: { groupPathsBy: "tag", categoryLinkSource: "auto", }, }, - webkey_v3: { - specPath: ".artifacts/openapi/zitadel/resources/webkey/v3alpha/webkey_service.swagger.json", - outputDir: "docs/apis/resources/webkey_service_v3", + webkey_v2: { + specPath: ".artifacts/openapi/zitadel/webkey/v2beta/webkey_service.swagger.json", + outputDir: "docs/apis/resources/webkey_service_v2", sidebarOptions: { groupPathsBy: "tag", categoryLinkSource: "auto", diff --git a/docs/sidebars.js b/docs/sidebars.js index 74bd19fc4a..92c7a00b2d 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -1,3 +1,19 @@ + +const sidebar_api_auth = require("./docs/apis/resources/auth/sidebar.ts").default +const sidebar_api_mgmt = require("./docs/apis/resources/mgmt/sidebar.ts").default +const sidebar_api_admin = require("./docs/apis/resources/admin/sidebar.ts").default +const sidebar_api_system = require("./docs/apis/resources/system/sidebar.ts").default + +const sidebar_api_user_service_v2 = require("./docs/apis/resources/user_service_v2/sidebar.ts").default +const sidebar_api_session_service_v2 = require("./docs/apis/resources/session_service_v2/sidebar.ts").default +const sidebar_api_oidc_service_v2 = require("./docs/apis/resources/oidc_service_v2/sidebar.ts").default +const sidebar_api_settings_service_v2 = require("./docs/apis/resources/settings_service_v2/sidebar.ts").default +const sidebar_api_feature_service_v2 = require("./docs/apis/resources/feature_service_v2/sidebar.ts").default +const sidebar_api_org_service_v2 = require("./docs/apis/resources/org_service_v2/sidebar.ts").default +const sidebar_api_idp_service_v2 = require("./docs/apis/resources/idp_service_v2/sidebar.ts").default +const sidebar_api_actions_v2 = require("./docs/apis/resources/action_service_v2/sidebar.ts").default +const sidebar_api_webkey_service_v2 = require("./docs/apis/resources/webkey_service_v2/sidebar.ts").default + module.exports = { guides: [ "guides/overview", @@ -123,6 +139,7 @@ module.exports = { items: [ "guides/manage/cloud/start", "guides/manage/cloud/instances", + "guides/manage/cloud/settings", "guides/manage/cloud/billing", "guides/manage/cloud/users", "guides/manage/cloud/support", @@ -454,6 +471,60 @@ module.exports = { ], }, "guides/integrate/external-audit-log", + { + type: "category", + label: "Actions", + link: { + type: "generated-index", + title: "Use Actions to integrate ZITADEL with your Favorite Services", + slug: "/guides/integrate/actions", + description: + "With the guides in this section you will learn how to use action to integrate Zitadel with your services.", + }, + collapsed: true, + items: [ + { + type: "doc", + id: "guides/integrate/actions/usage", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-request", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-request-manipulation", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-request-signature", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-response", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-response-manipulation", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-function", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-function-manipulation", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-event", + }, + { + type: "doc", + id: "guides/integrate/actions/migrate-from-v1", + }, + ], + }, ], }, { @@ -575,7 +646,7 @@ module.exports = { items: [ { type: "category", - label: "V1 (Generally Available)", + label: "V1", collapsed: false, link: { type: "generated-index", @@ -595,7 +666,7 @@ module.exports = { description: "The authentication API (aka Auth API) is used for all operations on the currently logged in user. The user id is taken from the sub claim in the token.", }, - items: require("./docs/apis/resources/auth/sidebar.ts"), + items: sidebar_api_auth, }, { type: "category", @@ -607,7 +678,7 @@ module.exports = { description: "The management API is as the name states the interface where systems can mutate IAM objects like, organizations, projects, clients, users and so on if they have the necessary access rights. To identify the current organization you can send a header x-zitadel-orgid or if no header is set, the organization of the authenticated user is set.", }, - items: require("./docs/apis/resources/mgmt/sidebar.ts"), + items: sidebar_api_mgmt, }, { type: "category", @@ -619,7 +690,7 @@ module.exports = { description: "This API is intended to configure and manage one ZITADEL instance itself.", }, - items: require("./docs/apis/resources/admin/sidebar.ts"), + items: sidebar_api_admin, }, { type: "category", @@ -633,13 +704,13 @@ module.exports = { "\n" + "Checkout the guide how to access the ZITADEL System API.", }, - items: require("./docs/apis/resources/system/sidebar.ts"), + items: sidebar_api_system, }, ], }, { type: "category", - label: "V2 (Generally Available)", + label: "V2", collapsed: false, link: { type: "doc", @@ -648,7 +719,7 @@ module.exports = { items: [ { type: "category", - label: "User Lifecycle", + label: "User", link: { type: "generated-index", title: "User Service API", @@ -656,11 +727,11 @@ module.exports = { description: "This API is intended to manage users in a ZITADEL instance.\n", }, - items: require("./docs/apis/resources/user_service_v2/sidebar.ts"), + items: sidebar_api_user_service_v2, }, { type: "category", - label: "Session Lifecycle", + label: "Session", link: { type: "generated-index", title: "Session Service API", @@ -668,11 +739,11 @@ module.exports = { description: "This API is intended to manage sessions in a ZITADEL instance.\n", }, - items: require("./docs/apis/resources/session_service_v2/sidebar.ts"), + items: sidebar_api_session_service_v2, }, { type: "category", - label: "OIDC Lifecycle", + label: "OIDC", link: { type: "generated-index", title: "OIDC Service API", @@ -680,11 +751,11 @@ module.exports = { description: "Get OIDC Auth Request details and create callback URLs.\n", }, - items: require("./docs/apis/resources/oidc_service_v2/sidebar.ts"), + items: sidebar_api_oidc_service_v2, }, { type: "category", - label: "Settings Lifecycle", + label: "Settings", link: { type: "generated-index", title: "Settings Service API", @@ -692,11 +763,11 @@ module.exports = { description: "This API is intended to manage settings in a ZITADEL instance.\n", }, - items: require("./docs/apis/resources/settings_service_v2/sidebar.ts"), + items: sidebar_api_settings_service_v2, }, { type: "category", - label: "Feature Lifecycle", + label: "Feature", link: { type: "generated-index", title: "Feature Service API", @@ -704,11 +775,11 @@ module.exports = { description: 'This API is intended to manage features for ZITADEL. Feature settings that are available on multiple "levels", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op.\n', }, - items: require("./docs/apis/resources/feature_service_v2/sidebar.ts"), + items: sidebar_api_feature_service_v2, }, { type: "category", - label: "Organization Lifecycle", + label: "Organization", link: { type: "generated-index", title: "Organization Service API", @@ -716,11 +787,11 @@ module.exports = { description: "This API is intended to manage organizations for ZITADEL. \n", }, - items: require("./docs/apis/resources/org_service_v2/sidebar.ts"), + items: sidebar_api_org_service_v2, }, { type: "category", - label: "Identity Provider Lifecycle", + label: "Identity Provider", link: { type: "generated-index", title: "Identity Provider Service API", @@ -728,57 +799,46 @@ module.exports = { description: "This API is intended to manage identity providers (IdPs) for ZITADEL.\n", }, - items: require("./docs/apis/resources/idp_service_v2/sidebar.ts"), - }, - ], - }, - { - type: "category", - label: "V3 (Preview)", - collapsed: false, - items: [ - { - type: "category", - label: "Action Lifecycle (Preview)", - link: { - type: "generated-index", - title: "Action Service API (Preview)", - slug: "/apis/resources/action_service_v3", - description: - "This API is intended to manage custom executions and targets (previously known as actions) in a ZITADEL instance.\n" + - "The version 3 of actions provide much more options to customize ZITADELs behaviour than previous action versions.\n" + - "Also, v3 actions are available instance-wide, whereas previous actions had to be managed for each organization individually\n" + - "ZITADEL doesn't restrict the implementation languages, tooling and runtime for v3 action executions anymore.\n" + - "Instead, it calls external endpoints which are implemented and maintained by action v3 users.\n" + - "\n" + - "This project is in Preview state. It can AND will continue breaking until the services provide the same functionality as the current actions.", - }, - items: [ - { - type: "doc", - id: "apis/actions/v3/usage", - }, - { - type: "doc", - id: "apis/actions/v3/testing-locally", - }, - ].concat( - require("./docs/apis/resources/action_service_v3/sidebar.ts") - ), + items: sidebar_api_idp_service_v2, }, { type: "category", - label: "Web key Lifecycle (Preview)", + label: "Web key (Beta)", link: { type: "generated-index", - title: "Web Key Service API (Preview)", - slug: "/apis/resources/webkey_service_v3", + title: "Web Key Service API (Beta)", + slug: "/apis/resources/webkey_service_v2", description: - "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n" + - "\n" + - "This project is in preview state. It can AND will continue breaking until a stable version is released.", + "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n" + + "\n" + + "This service is in beta state. It can AND will continue breaking until a stable version is released.\n"+ + "\n"+ + "The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.\n"+ + "\n"+ + "Please make sure to enable the `web_key` feature flag on your instance to use this service and that you're running ZITADEL V3.", }, - items: require("./docs/apis/resources/webkey_service_v3/sidebar.ts"), + items: sidebar_api_webkey_service_v2 + }, + { + type: "category", + label: "Action (Beta)", + link: { + type: "generated-index", + title: "Action Service API (Beta)", + slug: "/apis/resources/action_service_v2", + description: + "This API is intended to manage custom executions and targets (previously known as actions) in a ZITADEL instance.\n" + + "\n" + + "This service is in beta state. It can AND will continue breaking until a stable version is released.\n"+ + "\n" + + "The version 2 of actions provide much more options to customize ZITADELs behaviour than previous action versions.\n" + + "Also, v2 actions are available instance-wide, whereas previous actions had to be managed for each organization individually\n" + + "ZITADEL doesn't restrict the implementation languages, tooling and runtime for v2 action executions anymore.\n" + + "Instead, it calls external endpoints which are implemented and maintained by action v2 users.\n"+ + "\n" + + "Please make sure to enable the `actions` feature flag on your instance to use this service and that you're running Zitadel V3.", + }, + items: sidebar_api_actions_v2, }, ], }, @@ -854,8 +914,8 @@ module.exports = { }, { type: "link", - label: "Rate Limits (Cloud)", // The link label - href: "/legal/policies/rate-limit-policy", // The internal path + label: "Rate Limits (Cloud)", + href: "/legal/policies/rate-limit-policy", }, { type: "category", @@ -939,7 +999,6 @@ module.exports = { "self-hosting/manage/reverseproxy/traefik/traefik", "self-hosting/manage/reverseproxy/nginx/nginx", "self-hosting/manage/reverseproxy/caddy/caddy", - // "self-hosting/manage/reverseproxy/httpd/httpd", grpc NOT WORKING "self-hosting/manage/reverseproxy/cloudflare/cloudflare", "self-hosting/manage/reverseproxy/cloudflare_tunnel/cloudflare_tunnel", "self-hosting/manage/reverseproxy/zitadel_cloud/zitadel_cloud", diff --git a/docs/static/img/guides/console/invitehuman.png b/docs/static/img/guides/console/invitehuman.png new file mode 100644 index 0000000000..27857ced7e Binary files /dev/null and b/docs/static/img/guides/console/invitehuman.png differ diff --git a/docs/static/img/guides/console/setupauthmethod.png b/docs/static/img/guides/console/setupauthmethod.png new file mode 100644 index 0000000000..1c900f4662 Binary files /dev/null and b/docs/static/img/guides/console/setupauthmethod.png differ diff --git a/docs/static/img/guides/quickstart/20.png b/docs/static/img/guides/quickstart/20.png deleted file mode 100644 index 8ad3af1c50..0000000000 Binary files a/docs/static/img/guides/quickstart/20.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/21.png b/docs/static/img/guides/quickstart/21.png deleted file mode 100644 index b27a5d878d..0000000000 Binary files a/docs/static/img/guides/quickstart/21.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/38.png b/docs/static/img/guides/quickstart/38.png deleted file mode 100644 index 5c588a4235..0000000000 Binary files a/docs/static/img/guides/quickstart/38.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/39.png b/docs/static/img/guides/quickstart/39.png deleted file mode 100644 index 8de05371e6..0000000000 Binary files a/docs/static/img/guides/quickstart/39.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/41.png b/docs/static/img/guides/quickstart/41.png deleted file mode 100644 index 297cf57e8a..0000000000 Binary files a/docs/static/img/guides/quickstart/41.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/42.png b/docs/static/img/guides/quickstart/42.png deleted file mode 100644 index ff62d1001e..0000000000 Binary files a/docs/static/img/guides/quickstart/42.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/43.png b/docs/static/img/guides/quickstart/43.png deleted file mode 100644 index 1df261b869..0000000000 Binary files a/docs/static/img/guides/quickstart/43.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/44.png b/docs/static/img/guides/quickstart/44.png deleted file mode 100644 index 50bcb1a05e..0000000000 Binary files a/docs/static/img/guides/quickstart/44.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/45.png b/docs/static/img/guides/quickstart/45.png deleted file mode 100644 index 28ce863f0d..0000000000 Binary files a/docs/static/img/guides/quickstart/45.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/46.png b/docs/static/img/guides/quickstart/46.png deleted file mode 100644 index ca78190a75..0000000000 Binary files a/docs/static/img/guides/quickstart/46.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/47.png b/docs/static/img/guides/quickstart/47.png deleted file mode 100644 index a4ce71bde6..0000000000 Binary files a/docs/static/img/guides/quickstart/47.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/48.png b/docs/static/img/guides/quickstart/48.png deleted file mode 100644 index a9a48a496b..0000000000 Binary files a/docs/static/img/guides/quickstart/48.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/49.png b/docs/static/img/guides/quickstart/49.png deleted file mode 100644 index 05cf9af65d..0000000000 Binary files a/docs/static/img/guides/quickstart/49.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/50.png b/docs/static/img/guides/quickstart/50.png deleted file mode 100644 index 73ddb1e9ca..0000000000 Binary files a/docs/static/img/guides/quickstart/50.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/app_detail_overview_react.png b/docs/static/img/guides/quickstart/app_detail_overview_react.png new file mode 100644 index 0000000000..b529f5338c Binary files /dev/null and b/docs/static/img/guides/quickstart/app_detail_overview_react.png differ diff --git a/docs/static/img/guides/quickstart/create_instance.png b/docs/static/img/guides/quickstart/create_instance.png new file mode 100644 index 0000000000..7f7d1847e6 Binary files /dev/null and b/docs/static/img/guides/quickstart/create_instance.png differ diff --git a/docs/static/img/guides/quickstart/create_instance_admin.png b/docs/static/img/guides/quickstart/create_instance_admin.png new file mode 100644 index 0000000000..320228299b Binary files /dev/null and b/docs/static/img/guides/quickstart/create_instance_admin.png differ diff --git a/docs/static/img/guides/quickstart/create_instance_name.png b/docs/static/img/guides/quickstart/create_instance_name.png new file mode 100644 index 0000000000..b9a6c94181 Binary files /dev/null and b/docs/static/img/guides/quickstart/create_instance_name.png differ diff --git a/docs/static/img/guides/quickstart/create_project_react.png b/docs/static/img/guides/quickstart/create_project_react.png new file mode 100644 index 0000000000..afc52a9f35 Binary files /dev/null and b/docs/static/img/guides/quickstart/create_project_react.png differ diff --git a/docs/static/img/guides/quickstart/instance_dashboard.png b/docs/static/img/guides/quickstart/instance_dashboard.png new file mode 100644 index 0000000000..f57acb386d Binary files /dev/null and b/docs/static/img/guides/quickstart/instance_dashboard.png differ diff --git a/docs/static/img/guides/quickstart/onboarding_questions.png b/docs/static/img/guides/quickstart/onboarding_questions.png new file mode 100644 index 0000000000..47c7d69d64 Binary files /dev/null and b/docs/static/img/guides/quickstart/onboarding_questions.png differ diff --git a/docs/static/img/guides/quickstart/project_config_overview_react.png b/docs/static/img/guides/quickstart/project_config_overview_react.png new file mode 100644 index 0000000000..018b6e5b35 Binary files /dev/null and b/docs/static/img/guides/quickstart/project_config_overview_react.png differ diff --git a/docs/static/img/guides/quickstart/sign_in_instance.png b/docs/static/img/guides/quickstart/sign_in_instance.png new file mode 100644 index 0000000000..1ee0860957 Binary files /dev/null and b/docs/static/img/guides/quickstart/sign_in_instance.png differ diff --git a/docs/static/img/guides/quickstart/v3_10.png b/docs/static/img/guides/quickstart/v3_10.png deleted file mode 100644 index 4c05106001..0000000000 Binary files a/docs/static/img/guides/quickstart/v3_10.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/v3_11.png b/docs/static/img/guides/quickstart/v3_11.png deleted file mode 100644 index e49c81cc07..0000000000 Binary files a/docs/static/img/guides/quickstart/v3_11.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/v3_12.png b/docs/static/img/guides/quickstart/v3_12.png deleted file mode 100644 index eded356f0c..0000000000 Binary files a/docs/static/img/guides/quickstart/v3_12.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/v3_16.png b/docs/static/img/guides/quickstart/v3_16.png deleted file mode 100644 index 30be6d0635..0000000000 Binary files a/docs/static/img/guides/quickstart/v3_16.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/v3_9.png b/docs/static/img/guides/quickstart/v3_9.png deleted file mode 100644 index 7d754b2aec..0000000000 Binary files a/docs/static/img/guides/quickstart/v3_9.png and /dev/null differ diff --git a/docs/static/img/manuals/portal/customer_portal_add_admin.png b/docs/static/img/manuals/portal/customer_portal_add_admin.png index 8228810a56..cf3c497b13 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_add_admin.png and b/docs/static/img/manuals/portal/customer_portal_add_admin.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_add_domain.png b/docs/static/img/manuals/portal/customer_portal_add_domain.png index 2e83a599ca..2296cc629c 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_add_domain.png and b/docs/static/img/manuals/portal/customer_portal_add_domain.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_administrator_list.png b/docs/static/img/manuals/portal/customer_portal_administrator_list.png index e8fcc6bac8..c1aaf13a77 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_administrator_list.png and b/docs/static/img/manuals/portal/customer_portal_administrator_list.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_billing.png b/docs/static/img/manuals/portal/customer_portal_billing.png index 09f1cf9f9f..2f0e19068f 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_billing.png and b/docs/static/img/manuals/portal/customer_portal_billing.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_delete_admin.png b/docs/static/img/manuals/portal/customer_portal_delete_admin.png index cdaf75cf74..e3c6ac88a7 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_delete_admin.png and b/docs/static/img/manuals/portal/customer_portal_delete_admin.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_general_support.png b/docs/static/img/manuals/portal/customer_portal_general_support.png index c3e8241258..8aeb0a1711 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_general_support.png and b/docs/static/img/manuals/portal/customer_portal_general_support.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_instance_detail.png b/docs/static/img/manuals/portal/customer_portal_instance_detail.png index 48a9a2fd39..f33d7f26dd 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_instance_detail.png and b/docs/static/img/manuals/portal/customer_portal_instance_detail.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_instance_overview.png b/docs/static/img/manuals/portal/customer_portal_instance_overview.png index 973c6b9205..7360fadbb0 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_instance_overview.png and b/docs/static/img/manuals/portal/customer_portal_instance_overview.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_invoices.png b/docs/static/img/manuals/portal/customer_portal_invoices.png index 80942fdb24..4cb72e118d 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_invoices.png and b/docs/static/img/manuals/portal/customer_portal_invoices.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_settings_general.png b/docs/static/img/manuals/portal/customer_portal_settings_general.png index f1daa340db..a9aaf068cd 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_settings_general.png and b/docs/static/img/manuals/portal/customer_portal_settings_general.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_upgrade_tier.png b/docs/static/img/manuals/portal/customer_portal_upgrade_tier.png index 8f84c79545..382a5bcbb6 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_upgrade_tier.png and b/docs/static/img/manuals/portal/customer_portal_upgrade_tier.png differ diff --git a/docs/static/img/zitadel_cluster_architecture.png b/docs/static/img/zitadel_cluster_architecture.png index a968610b70..a6d63af501 100644 Binary files a/docs/static/img/zitadel_cluster_architecture.png and b/docs/static/img/zitadel_cluster_architecture.png differ diff --git a/docs/static/img/zitadel_multicluster_architecture.png b/docs/static/img/zitadel_multicluster_architecture.png index 3efccde609..22b2ead4f5 100644 Binary files a/docs/static/img/zitadel_multicluster_architecture.png and b/docs/static/img/zitadel_multicluster_architecture.png differ diff --git a/docs/vercel.json b/docs/vercel.json index 58a13e4b8c..039dc02476 100644 --- a/docs/vercel.json +++ b/docs/vercel.json @@ -24,8 +24,8 @@ { "source": "/docs/apis/auth/:slug*", "destination": "/docs/apis/resources/auth/:slug*", "permanent": true }, { "source": "/docs/apis/system/:slug*", "destination": "/docs/apis/resources/system/:slug*", "permanent": true }, { "source": "/docs/apis/admin/:slug*", "destination": "/docs/apis/resources/admin/:slug*", "permanent": true }, - { "source": "/docs/apis/actionsv2/introduction", "destination": "/docs/apis/actions/v3/usage", "permanent": true }, - { "source": "/docs/apis/actionsv2/execution-local", "destination": "/docs/apis/actions/v3/testing-locally", "permanent": true }, + { "source": "/docs/apis/actionsv2/introduction", "destination": "/docs/apis/actions/v2/usage", "permanent": true }, + { "source": "/docs/apis/actionsv2/execution-local", "destination": "/docs/apis/actions/v2/testing-locally", "permanent": true }, { "source": "/docs/guides/integrate/human-users", "destination": "/docs/guides/integrate/login", "permanent": true }, { "source": "/docs/guides/solution-scenarios/device-authorization", "destination": "/docs/guides/integrate/login/oidc/device-authorization", "permanent": true }, { "source": "/docs/guides/integrate/oauth-recommended-flows", "destination": "/docs/guides/integrate/login/oidc/oauth-recommended-flows", "permanent": true }, diff --git a/docs/yarn.lock b/docs/yarn.lock index a308fc3d06..ad31e03b5e 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -185,6 +185,15 @@ "@babel/highlight" "^7.24.7" picocolors "^1.0.0" +"@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed" @@ -389,11 +398,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2" integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg== +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + "@babel/helper-validator-option@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6" @@ -410,12 +429,12 @@ "@babel/types" "^7.24.7" "@babel/helpers@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.7.tgz#aa2ccda29f62185acb5d42fb4a3a1b1082107416" - integrity sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg== + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.0.tgz#53d156098defa8243eab0f32fa17589075a1b808" + integrity sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg== dependencies: - "@babel/template" "^7.24.7" - "@babel/types" "^7.24.7" + "@babel/template" "^7.27.0" + "@babel/types" "^7.27.0" "@babel/highlight@^7.24.7": version "7.24.7" @@ -432,6 +451,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85" integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw== +"@babel/parser@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec" + integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== + dependencies: + "@babel/types" "^7.27.0" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz#fd059fd27b184ea2b4c7e646868a9a381bbc3055" @@ -1192,9 +1218,9 @@ regenerator-runtime "^0.14.0" "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.22.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" - integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" + integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== dependencies: regenerator-runtime "^0.14.0" @@ -1207,6 +1233,15 @@ "@babel/parser" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/template@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4" + integrity sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/parser" "^7.27.0" + "@babel/types" "^7.27.0" + "@babel/traverse@^7.22.8", "@babel/traverse@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" @@ -1232,6 +1267,14 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" +"@babel/types@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559" + integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@braintree/sanitize-url@^6.0.1": version "6.0.4" resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" @@ -3409,9 +3452,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001629: - version "1.0.30001636" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78" - integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg== + version "1.0.30001702" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz" + integrity sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA== ccount@^2.0.0: version "2.0.1" @@ -6153,9 +6196,9 @@ ignore@^5.2.0, ignore@^5.2.4: integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== image-size@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.1.1.tgz#ddd67d4dc340e52ac29ce5f546a09f4e29e840ac" - integrity sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ== + version "1.2.1" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.2.1.tgz#ee118aedfe666db1a6ee12bed5821cde3740276d" + integrity sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw== dependencies: queue "6.0.2" diff --git a/e2e/config/host.docker.internal/zitadel.yaml b/e2e/config/host.docker.internal/zitadel.yaml index cb7e985be1..203dd16437 100644 --- a/e2e/config/host.docker.internal/zitadel.yaml +++ b/e2e/config/host.docker.internal/zitadel.yaml @@ -5,12 +5,23 @@ ExternalDomain: host.docker.internal ExternalSecure: false Database: - cockroach: + postgres: # This makes the e2e config reusable with an out-of-docker zitadel process and an /etc/hosts entry - Host: host.docker.internal - EventPushConnRatio: 0.2 + Host: host.docker.internal + Port: 5432 MaxOpenConns: 15 MaxIdleConns: 10 + Database: zitadel + User: + Username: zitadel + Password: zitadel + SSL: + Mode: disable + Admin: + Username: postgres + Password: postgres + SSL: + Mode: disable TLS: Enabled: false diff --git a/e2e/config/localhost/docker-compose.yaml b/e2e/config/localhost/docker-compose.yaml index 525404938d..41334d92f9 100644 --- a/e2e/config/localhost/docker-compose.yaml +++ b/e2e/config/localhost/docker-compose.yaml @@ -30,14 +30,15 @@ services: db: restart: 'always' - image: 'cockroachdb/cockroach:latest-v24.3' - command: 'start-single-node --insecure --http-addr :9090' + image: 'postgres:17-alpine' + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:9090/health?ready=1'] + test: ["CMD-SHELL", "pg_isready", "-d", "zitadel", "-U", "postgres"] interval: '10s' timeout: '30s' retries: 5 start_period: '20s' ports: - - "26257:26257" - - "9090:9090" + - "5432:5432" diff --git a/e2e/config/localhost/zitadel.yaml b/e2e/config/localhost/zitadel.yaml index 649f35fa9d..966bb4f6b7 100644 --- a/e2e/config/localhost/zitadel.yaml +++ b/e2e/config/localhost/zitadel.yaml @@ -5,12 +5,24 @@ ExternalDomain: localhost ExternalSecure: false Database: - cockroach: + postgres: # This makes the e2e config reusable with an out-of-docker zitadel process and an /etc/hosts entry Host: host.docker.internal - EventPushConnRatio: 0.2 + Port: 5432 + database: zitadel MaxOpenConns: 15 MaxIdleConns: 10 + Database: zitadel + User: + Username: zitadel + Password: zitadel + SSL: + Mode: disable + Admin: + Username: postgres + Password: postgres + SSL: + Mode: disable TLS: Enabled: false diff --git a/go.mod b/go.mod index 6af7200325..99df9ad86f 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/zitadel/zitadel go 1.23.7 -toolchain go1.24.1 - require ( cloud.google.com/go/profiler v0.4.2 cloud.google.com/go/storage v1.51.0 @@ -26,6 +24,7 @@ require ( 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/go-chi/chi/v5 v5.2.1 github.com/go-jose/go-jose/v4 v4.0.5 @@ -75,7 +74,7 @@ require ( github.com/zitadel/logging v0.6.2 github.com/zitadel/oidc/v3 v3.36.1 github.com/zitadel/passwap v0.7.0 - github.com/zitadel/saml v0.3.3 + github.com/zitadel/saml v0.3.5 github.com/zitadel/schema v1.3.1 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 @@ -148,6 +147,7 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect github.com/zenazn/goji v1.0.1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect @@ -189,7 +189,6 @@ require ( 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 - github.com/gofrs/flock v0.12.1 // indirect github.com/golang/geo v0.0.0-20250319145452-ed1c8b99c3d7 // indirect github.com/google/uuid v1.6.0 github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect diff --git a/go.sum b/go.sum index 8fb94c9afe..cb8f0cf4bd 100644 --- a/go.sum +++ b/go.sum @@ -214,6 +214,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fergusstrange/embedded-postgres v1.30.0 h1:ewv1e6bBlqOIYtgGgRcEnNDpfGlmfPxB8T3PO9tV68Q= +github.com/fergusstrange/embedded-postgres v1.30.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -785,6 +787,8 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= @@ -805,8 +809,8 @@ github.com/zitadel/oidc/v3 v3.36.1 h1:1AT1NqKKEqAwx4GmKJZ9fYkWH2WIn/VKMfQ46nBtRf github.com/zitadel/oidc/v3 v3.36.1/go.mod h1:dApGZLvWZTHRuxmcbQlW5d2XVjVYR3vGOdq536igmTs= github.com/zitadel/passwap v0.7.0 h1:TQTr9TV75PLATGICor1g5hZDRNHRvB9t0Hn4XkiR7xQ= github.com/zitadel/passwap v0.7.0/go.mod h1:/NakQNYahdU+YFEitVD6mlm8BLfkiIT+IM5wgClRoAY= -github.com/zitadel/saml v0.3.3 h1:Cn+1ZNeWlzMM7wxUxJfgNjXSW+Yu6UD4zWbpySA5GQM= -github.com/zitadel/saml v0.3.3/go.mod h1:QqKcguOt7mMVI6tkEfpkyzwnYRdlmn3kYQj3VTPUw1g= +github.com/zitadel/saml v0.3.5 h1:L1RKWS5y66cGepVxUGjx/WSBOtrtSpRA/J3nn5BJLOY= +github.com/zitadel/saml v0.3.5/go.mod h1:ybs3e4tIWdYgSYBpuCsvf3T4FNDfbXYM+GPv5vIpHYk= github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= diff --git a/internal/admin/repository/eventsourcing/handler/handler.go b/internal/admin/repository/eventsourcing/handler/handler.go index ec268c25a1..76584b55b0 100644 --- a/internal/admin/repository/eventsourcing/handler/handler.go +++ b/internal/admin/repository/eventsourcing/handler/handler.go @@ -2,9 +2,13 @@ package handler import ( "context" + "fmt" "time" + "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 +61,13 @@ func Start(ctx context.Context) { } 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 admin 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("admin projection done") } return nil } diff --git a/internal/api/api.go b/internal/api/api.go index 15d6c5b996..62d3e14b35 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -15,7 +15,7 @@ import ( healthpb "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/reflection" - internal_authz "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" http_util "github.com/zitadel/zitadel/internal/api/http" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" @@ -29,7 +29,7 @@ import ( type API struct { port uint16 grpcServer *grpc.Server - verifier internal_authz.APITokenVerifier + verifier authz.APITokenVerifier health healthCheck router *mux.Router hostHeaders []string @@ -72,8 +72,9 @@ func New( port uint16, router *mux.Router, queries *query.Queries, - verifier internal_authz.APITokenVerifier, - authZ internal_authz.Config, + verifier authz.APITokenVerifier, + systemAuthz authz.Config, + authZ authz.Config, tlsConfig *tls.Config, externalDomain string, hostHeaders []string, @@ -89,7 +90,7 @@ func New( hostHeaders: hostHeaders, } - api.grpcServer = server.CreateServer(api.verifier, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService()) + api.grpcServer = server.CreateServer(api.verifier, systemAuthz, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService()) api.grpcGateway, err = server.CreateGateway(ctx, port, hostHeaders, accessInterceptor, tlsConfig) if err != nil { return nil, err diff --git a/internal/api/assets/asset.go b/internal/api/assets/asset.go index 57ad3710bc..8c3c51c6aa 100644 --- a/internal/api/assets/asset.go +++ b/internal/api/assets/asset.go @@ -94,13 +94,13 @@ func DefaultErrorHandler(translator *i18n.Translator) func(w http.ResponseWriter } } -func NewHandler(commands *command.Commands, verifier authz.APITokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler { +func NewHandler(commands *command.Commands, verifier authz.APITokenVerifier, systemAuthCOnfig authz.Config, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler { translator, err := i18n.NewZitadelTranslator(language.English) logging.OnError(err).Panic("unable to get translator") h := &Handler{ commands: commands, errorHandler: DefaultErrorHandler(translator), - authInterceptor: http_mw.AuthorizationInterceptor(verifier, authConfig), + authInterceptor: http_mw.AuthorizationInterceptor(verifier, systemAuthCOnfig, authConfig), idGenerator: idGenerator, storage: storage, query: queries, @@ -129,8 +129,10 @@ func (l *publicFileDownloader) ResourceOwner(_ context.Context, ownerPath string return ownerPath } -const maxMemory = 2 << 20 -const paramFile = "file" +const ( + maxMemory = 2 << 20 + paramFile = "file" +) func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/authz/authorization.go b/internal/api/authz/authorization.go index 2099b3e426..ea20a2438f 100644 --- a/internal/api/authz/authorization.go +++ b/internal/api/authz/authorization.go @@ -4,8 +4,11 @@ import ( "context" "fmt" "reflect" + "slices" "strings" + "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -16,10 +19,10 @@ const ( // CheckUserAuthorization verifies that: // - the token is active, -// - the organisation (**either** provided by ID or verified domain) exists +// - the organization (**either** provided by ID or verified domain) exists // - the user is permitted to call the requested endpoint (permission option in proto) // it will pass the [CtxData] and permission of the user into the ctx [context.Context] -func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier APITokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) { +func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier APITokenVerifier, systemRolePermissionMapping []RoleMapping, rolePermissionMapping []RoleMapping, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) { ctx, span := tracing.NewServerInterceptorSpan(ctx) defer func() { span.EndWithError(err) }() @@ -30,11 +33,12 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, if requiredAuthOption.Permission == authenticated { return func(parent context.Context) context.Context { + parent = addGetSystemUserRolesToCtx(parent, systemRolePermissionMapping, ctxData) return context.WithValue(parent, dataKey, ctxData) }, nil } - requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig.RolePermissionMappings, ctxData, ctxData.OrgID) + requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, systemRolePermissionMapping, rolePermissionMapping, ctxData, ctxData.OrgID) if err != nil { return nil, err } @@ -50,6 +54,7 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, parent = context.WithValue(parent, dataKey, ctxData) parent = context.WithValue(parent, allPermissionsKey, allPermissions) parent = context.WithValue(parent, requestPermissionsKey, requestedPermissions) + parent = addGetSystemUserRolesToCtx(parent, systemRolePermissionMapping, ctxData) return parent }, nil } @@ -125,3 +130,43 @@ func GetAllPermissionCtxIDs(perms []string) []string { } return ctxIDs } + +type SystemUserPermissionsDBQuery struct { + MemberType string `json:"member_type"` + AggregateID string `json:"aggregate_id"` + ObjectID string `json:"object_id"` + Permissions []string `json:"permissions"` +} + +func addGetSystemUserRolesToCtx(ctx context.Context, systemUserRoleMap []RoleMapping, ctxData CtxData) context.Context { + if len(ctxData.SystemMemberships) == 0 { + return ctx + } + systemUserPermissions := make([]SystemUserPermissionsDBQuery, len(ctxData.SystemMemberships)) + for i, systemPerm := range ctxData.SystemMemberships { + permissions := make([]string, 0, len(systemPerm.Roles)) + for _, role := range systemPerm.Roles { + permissions = append(permissions, getPermissionsFromRole(systemUserRoleMap, role)...) + } + slices.Sort(permissions) + permissions = slices.Compact(permissions) + + systemUserPermissions[i].MemberType = systemPerm.MemberType.String() + systemUserPermissions[i].AggregateID = systemPerm.AggregateID + systemUserPermissions[i].Permissions = permissions + } + return context.WithValue(ctx, systemUserRolesKey, systemUserPermissions) +} + +func GetSystemUserPermissions(ctx context.Context) []SystemUserPermissionsDBQuery { + getSystemUserRolesFuncValue := ctx.Value(systemUserRolesKey) + if getSystemUserRolesFuncValue == nil { + return nil + } + systemUserRoles, ok := getSystemUserRolesFuncValue.([]SystemUserPermissionsDBQuery) + if !ok { + logging.WithFields("Authz").Error("unable to cast []SystemUserPermissionsDBQuery") + return nil + } + return systemUserRoles +} diff --git a/internal/api/authz/context.go b/internal/api/authz/context.go index ff401f8862..d6528cd017 100644 --- a/internal/api/authz/context.go +++ b/internal/api/authz/context.go @@ -22,6 +22,7 @@ const ( dataKey key = 2 allPermissionsKey key = 3 instanceKey key = 4 + systemUserRolesKey key = 5 ) type CtxData struct { @@ -50,7 +51,8 @@ type Memberships []*Membership type Membership struct { MemberType MemberType AggregateID string - //ObjectID differs from aggregate id if object is sub of an aggregate + InstanceID string + // ObjectID differs from aggregate id if object is sub of an aggregate ObjectID string Roles []string diff --git a/internal/api/authz/permissions.go b/internal/api/authz/permissions.go index e96a7b256b..904fbbc33a 100644 --- a/internal/api/authz/permissions.go +++ b/internal/api/authz/permissions.go @@ -7,8 +7,8 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) { - requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, GetCtxData(ctx), orgID) +func CheckPermission(ctx context.Context, resolver MembershipsResolver, systemUserRoleMapping []RoleMapping, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) { + requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, systemUserRoleMapping, roleMappings, GetCtxData(ctx), orgID) if err != nil { return err } @@ -22,7 +22,7 @@ func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMapp // getUserPermissions retrieves the memberships of the authenticated user (on instance and provided organisation level), // and maps them to permissions. It will return the requested permission(s) and all other granted permissions separately. -func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) { +func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, systemUserRoleMappings []RoleMapping, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -31,7 +31,7 @@ func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requi } if ctxData.SystemMemberships != nil { - requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, roleMappings) + requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, systemUserRoleMappings) return requestedPermissions, allPermissions, nil } diff --git a/internal/api/authz/permissions_test.go b/internal/api/authz/permissions_test.go index 7919747de6..93243d0c09 100644 --- a/internal/api/authz/permissions_test.go +++ b/internal/api/authz/permissions_test.go @@ -120,7 +120,7 @@ func Test_GetUserPermissions(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, perms, err := getUserPermissions(context.Background(), tt.args.membershipsResolver, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID) + _, perms, err := getUserPermissions(context.Background(), tt.args.membershipsResolver, tt.args.requiredPerm, nil, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID) if tt.wantErr && err == nil { t.Errorf("got wrong result, should get err: actual: %v ", err) diff --git a/internal/api/grpc/resources/action/v3alpha/execution.go b/internal/api/grpc/action/v2beta/execution.go similarity index 59% rename from internal/api/grpc/resources/action/v3alpha/execution.go rename to internal/api/grpc/action/v2beta/execution.go index 94ad17c2f0..5477a8128e 100644 --- a/internal/api/grpc/resources/action/v3alpha/execution.go +++ b/internal/api/grpc/action/v2beta/execution.go @@ -3,33 +3,21 @@ package action import ( "context" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/api/authz" - resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionRequest) (*action.SetExecutionResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } - reqTargets := req.GetExecution().GetTargets() + reqTargets := req.GetTargets() targets := make([]*execution.Target, len(reqTargets)) for i, target := range reqTargets { - switch t := target.GetType().(type) { - case *action.ExecutionTargetType_Include: - include, err := conditionToInclude(t.Include) - if err != nil { - return nil, err - } - targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeInclude, Target: include} - case *action.ExecutionTargetType_Target: - targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: t.Target} - } + targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: target} } set := &command.SetExecution{ Targets: targets, @@ -56,63 +44,23 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque return nil, err } return &action.SetExecutionResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + SetDate: timestamppb.New(details.EventDate), }, nil } -func conditionToInclude(cond *action.Condition) (string, error) { - switch t := cond.GetConditionType().(type) { - case *action.Condition_Request: - cond := executionConditionFromRequest(t.Request) - if err := cond.IsValid(); err != nil { - return "", err - } - return cond.ID(domain.ExecutionTypeRequest), nil - case *action.Condition_Response: - cond := executionConditionFromResponse(t.Response) - if err := cond.IsValid(); err != nil { - return "", err - } - return cond.ID(domain.ExecutionTypeRequest), nil - case *action.Condition_Event: - cond := executionConditionFromEvent(t.Event) - if err := cond.IsValid(); err != nil { - return "", err - } - return cond.ID(), nil - case *action.Condition_Function: - cond := command.ExecutionFunctionCondition(t.Function.GetName()) - if err := cond.IsValid(); err != nil { - return "", err - } - return cond.ID(), nil - default: - return "", zerrors.ThrowInvalidArgument(nil, "ACTION-9BBob", "Errors.Execution.ConditionInvalid") - } -} - func (s *Server) ListExecutionFunctions(ctx context.Context, _ *action.ListExecutionFunctionsRequest) (*action.ListExecutionFunctionsResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } return &action.ListExecutionFunctionsResponse{ Functions: s.ListActionFunctions(), }, nil } func (s *Server) ListExecutionMethods(ctx context.Context, _ *action.ListExecutionMethodsRequest) (*action.ListExecutionMethodsResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } return &action.ListExecutionMethodsResponse{ Methods: s.ListGRPCMethods(), }, nil } func (s *Server) ListExecutionServices(ctx context.Context, _ *action.ListExecutionServicesRequest) (*action.ListExecutionServicesResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } return &action.ListExecutionServicesResponse{ Services: s.ListGRPCServices(), }, nil diff --git a/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go new file mode 100644 index 0000000000..0c5018dbb6 --- /dev/null +++ b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go @@ -0,0 +1,1309 @@ +//go:build integration + +package action_test + +import ( + "context" + "encoding/base64" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/crewjam/saml" + "github.com/crewjam/saml/samlsp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" + oidc_api "github.com/zitadel/zitadel/internal/api/oidc" + saml_api "github.com/zitadel/zitadel/internal/api/saml" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/internal/query" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/app" + "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/metadata" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" + saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +const ( + redirectURIImplicit = "http://localhost:9999/callback" +) + +var ( + loginV2 = &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}} +) + +func TestServer_ExecutionTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + fullMethod := action.ActionService_GetTarget_FullMethodName + + tests := []struct { + name string + ctx context.Context + dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) (closeF func(), calledF func() bool) + clean func(context.Context) + req *action.GetTargetRequest + want *action.GetTargetResponse + wantErr bool + }{ + { + name: "GetTarget, request and response, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) { + + orgID := instance.DefaultOrg.Id + projectID := "" + userID := instance.Users.Get(integration.UserTypeIAMOwner).ID + + // create target for target changes + targetCreatedName := gofakeit.Name() + targetCreatedURL := "https://nonexistent" + + targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) + + // request received by target + wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: middleware.Message{Message: request}} + changedRequest := &action.GetTargetRequest{Id: targetCreated.GetId()} + // replace original request with different targetID + urlRequest, closeRequest, calledRequest, _ := integration.TestServerCallProto(wantRequest, 0, http.StatusOK, changedRequest) + + targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, false) + + waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), []string{targetRequest.GetId()}) + + // expected response from the GetTarget + expectedResponse := &action.GetTargetResponse{ + Target: &action.Target{ + Id: targetCreated.GetId(), + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + }, + } + + changedResponse := &action.GetTargetResponse{ + Target: &action.Target{ + Id: "changed", + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + }, + } + // content for update + response.Target = &action.Target{ + Id: "changed", + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + } + + // response received by target + wantResponse := &middleware.ContextInfoResponse{ + FullMethod: fullMethod, + InstanceID: instance.ID(), + OrgID: orgID, + ProjectID: projectID, + UserID: userID, + Request: middleware.Message{Message: changedRequest}, + Response: middleware.Message{Message: expectedResponse}, + } + // after request with different targetID, return changed response + targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCallProto(wantResponse, 0, http.StatusOK, changedResponse) + + targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, false) + waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), []string{targetResponse.GetId()}) + return func() { + closeRequest() + closeResponse() + }, func() bool { + if calledRequest() != 1 { + return false + } + if calledResponse() != 1 { + return false + } + return true + } + }, + clean: func(ctx context.Context) { + instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) + instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) + }, + req: &action.GetTargetRequest{ + Id: "something", + }, + want: &action.GetTargetResponse{ + // defined in the dependency function + }, + }, + { + name: "GetTarget, request, interrupt", + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) { + orgID := instance.DefaultOrg.Id + projectID := "" + userID := instance.Users.Get(integration.UserTypeIAMOwner).ID + + // request received by target + wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: middleware.Message{Message: request}} + urlRequest, closeRequest, calledRequest, _ := integration.TestServerCallProto(wantRequest, 0, http.StatusInternalServerError, nil) + + targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), []string{targetRequest.GetId()}) + // GetTarget with used target + request.Id = targetRequest.GetId() + return func() { + closeRequest() + }, func() bool { + return calledRequest() == 1 + } + }, + clean: func(ctx context.Context) { + instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) + }, + req: &action.GetTargetRequest{}, + wantErr: true, + }, + { + name: "GetTarget, response, interrupt", + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) { + orgID := instance.DefaultOrg.Id + projectID := "" + userID := instance.Users.Get(integration.UserTypeIAMOwner).ID + + // create target for target changes + targetCreatedName := gofakeit.Name() + targetCreatedURL := "https://nonexistent" + + targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) + + // GetTarget with used target + request.Id = targetCreated.GetId() + + // expected response from the GetTarget + expectedResponse := &action.GetTargetResponse{ + Target: &action.Target{ + Id: targetCreated.GetId(), + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + }, + } + + // response received by target + wantResponse := &middleware.ContextInfoResponse{ + FullMethod: fullMethod, + InstanceID: instance.ID(), + OrgID: orgID, + ProjectID: projectID, + UserID: userID, + Request: middleware.Message{Message: request}, + Response: middleware.Message{Message: expectedResponse}, + } + // after request with different targetID, return changed response + targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCallProto(wantResponse, 0, http.StatusInternalServerError, nil) + + targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), []string{targetResponse.GetId()}) + return func() { + closeResponse() + }, func() bool { + return calledResponse() == 1 + } + }, + clean: func(ctx context.Context) { + instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) + }, + req: &action.GetTargetRequest{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + closeF, calledF := tt.dep(tt.ctx, tt.req, tt.want) + defer closeF() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV2beta.GetTarget(tt.ctx, tt.req) + if tt.wantErr { + require.Error(ttt, err) + return + } + require.NoError(ttt, err) + assert.EqualExportedValues(ttt, tt.want.GetTarget(), got.GetTarget()) + + }, retryDuration, tick, "timeout waiting for expected execution result") + + if tt.clean != nil { + tt.clean(tt.ctx) + } + require.True(t, calledF()) + }) + } +} + +func TestServer_ExecutionTarget_Event(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + event := "session.added" + urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 0, http.StatusOK, nil) + defer closeF() + + targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true) + waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()}) + + tests := []struct { + name string + ctx context.Context + eventCount int + expectedCalls int + clean func(context.Context) + wantErr bool + }{ + { + name: "event, 1 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 1, + expectedCalls: 1, + }, + { + name: "event, 5 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 5, + expectedCalls: 5, + }, + { + name: "event, 50 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 50, + expectedCalls: 50, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // reset the count of the target + resetF() + + for i := 0; i < tt.eventCount; i++ { + _, err := instance.Client.SessionV2.CreateSession(tt.ctx, &session.CreateSessionRequest{}) + require.NoError(t, err) + } + + // wait for called target + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + assert.True(ttt, calledF() == tt.expectedCalls) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ExecutionTarget_Event_LongerThanTargetTimeout(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + event := "session.added" + // call takes longer than timeout of target + urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 5*time.Second, http.StatusOK, nil) + defer closeF() + + targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true) + waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()}) + + tests := []struct { + name string + ctx context.Context + eventCount int + expectedCalls int + clean func(context.Context) + wantErr bool + }{ + { + name: "event, 1 session.added, error logs", + ctx: isolatedIAMOwnerCTX, + eventCount: 1, + expectedCalls: 1, + }, + { + name: "event, 5 session.added, error logs", + ctx: isolatedIAMOwnerCTX, + eventCount: 5, + expectedCalls: 5, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // reset the count of the target + resetF() + + for i := 0; i < tt.eventCount; i++ { + _, err := instance.Client.SessionV2.CreateSession(tt.ctx, &session.CreateSessionRequest{}) + require.NoError(t, err) + } + + // wait for called target + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + assert.True(ttt, calledF() == tt.expectedCalls) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ExecutionTarget_Event_LongerThanTransactionTimeout(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + event := "session.added" + urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 1*time.Second, http.StatusOK, nil) + defer closeF() + + targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true) + waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()}) + + tests := []struct { + name string + ctx context.Context + eventCount int + expectedCalls int + clean func(context.Context) + wantErr bool + }{ + { + name: "event, 1 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 1, + expectedCalls: 1, + }, + { + name: "event, 5 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 5, + expectedCalls: 5, + }, + { + name: "event, 5 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 5, + expectedCalls: 5, + }, + { + name: "event, 20 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 20, + expectedCalls: 20, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // reset the count of the target + resetF() + + for i := 0; i < tt.eventCount; i++ { + _, err := instance.Client.SessionV2.CreateSession(tt.ctx, &session.CreateSessionRequest{}) + require.NoError(t, err) + } + + // wait for called target + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + assert.True(ttt, calledF() == tt.expectedCalls) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *integration.Instance, condition *action.Condition, targets []string) { + instance.SetExecution(ctx, t, condition, targets) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV2beta.ListExecutions(ctx, &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{ + {Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{Conditions: []*action.Condition{condition}}, + }}, + }, + }) + if !assert.NoError(ttt, err) { + return + } + if !assert.Len(ttt, got.GetResult(), 1) { + return + } + gotTargets := got.GetResult()[0].GetTargets() + // always first check length, otherwise its failed anyway + if assert.Len(ttt, gotTargets, len(targets)) { + for i := range targets { + assert.EqualExportedValues(ttt, targets[i], gotTargets[i]) + } + } + }, retryDuration, tick, "timeout waiting for expected execution result") + return +} + +func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Instance, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { + resp := instance.CreateTarget(ctx, t, "", endpoint, ty, interrupt) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV2beta.ListTargets(ctx, &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{ + {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{TargetIds: []string{resp.GetId()}}, + }}, + }, + }) + if !assert.NoError(ttt, err) { + return + } + if !assert.Len(ttt, got.GetResult(), 1) { + return + } + config := got.GetResult()[0] + assert.Equal(ttt, config.GetEndpoint(), endpoint) + switch ty { + case domain.TargetTypeWebhook: + if !assert.NotNil(ttt, config.GetRestWebhook()) { + return + } + assert.Equal(ttt, interrupt, config.GetRestWebhook().GetInterruptOnError()) + case domain.TargetTypeAsync: + assert.NotNil(ttt, config.GetRestAsync()) + case domain.TargetTypeCall: + if !assert.NotNil(ttt, config.GetRestCall()) { + return + } + assert.Equal(ttt, interrupt, config.GetRestCall().GetInterruptOnError()) + } + }, retryDuration, tick, "timeout waiting for expected execution result") + return resp +} + +func conditionRequestFullMethod(fullMethod string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: fullMethod, + }, + }, + }, + } +} + +func conditionResponseFullMethod(fullMethod string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: fullMethod, + }, + }, + }, + } +} + +func conditionEvent(event string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Event{ + Event: event, + }, + }, + }, + } +} + +func conditionFunction(function string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Function{ + Function: &action.FunctionExecution{ + Name: function, + }, + }, + } +} + +func TestServer_ExecutionTargetPreUserinfo(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) + + client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2) + require.NoError(t, err) + + type want struct { + addedClaims map[string]any + addedLogClaims map[string][]string + setUserMetadata []*metadata.Metadata + } + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) + req *oidc_pb.CreateCallbackRequest + want want + wantErr bool + }{ + { + name: "append claim", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + AppendClaims: []*oidc_api.AppendClaim{ + {Key: "added", Value: "value"}, + }, + } + return expectPreUserinfoExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: map[string]any{ + "added": "value", + }, + }, + wantErr: false, + }, + { + name: "append log claim", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + AppendLogClaims: []string{ + "addedLog", + }, + } + return expectPreUserinfoExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedLogClaims: map[string][]string{ + "urn:zitadel:iam:action:function/preuserinfo:log": {"addedLog"}, + }, + }, + wantErr: false, + }, + { + name: "set user metadata", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + } + return expectPreUserinfoExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + setUserMetadata: []*metadata.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + }, + wantErr: false, + }, + { + name: "full usage", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + AppendLogClaims: []string{ + "addedLog1", + "addedLog2", + "addedLog3", + }, + AppendClaims: []*oidc_api.AppendClaim{ + {Key: "added1", Value: "value1"}, + {Key: "added2", Value: "value2"}, + {Key: "added3", Value: "value3"}, + }, + } + return expectPreUserinfoExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: map[string]any{ + "added1": "value1", + "added2": "value2", + "added3": "value3", + }, + setUserMetadata: []*metadata.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + addedLogClaims: map[string][]string{ + "urn:zitadel:iam:action:function/preuserinfo:log": {"addedLog1", "addedLog2", "addedLog3"}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID, closeF := tt.dep(isolatedIAMCtx, t, tt.req) + defer closeF() + + got, err := instance.Client.OIDCv2.CreateCallback(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + callbackUrl, err := url.Parse(strings.Replace(got.GetCallbackUrl(), "#", "?", 1)) + require.NoError(t, err) + claims := getIDTokenClaimsFromCallbackURL(tt.ctx, t, instance, client.GetClientId(), callbackUrl) + + for k, v := range tt.want.addedClaims { + value, ok := claims[k] + if !assert.True(t, ok) { + return + } + assert.Equal(t, v, value) + } + for k, v := range tt.want.addedLogClaims { + value, ok := claims[k] + if !assert.True(t, ok) { + return + } + assert.ElementsMatch(t, v, value) + } + if len(tt.want.setUserMetadata) > 0 { + checkForSetMetadata(isolatedIAMCtx, t, instance, userID, tt.want.setUserMetadata) + } + }) + } +} + +func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) { + userEmail := gofakeit.Email() + userPhone := "+41" + gofakeit.Phone() + userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone) + + sessionResp := createSession(ctx, t, instance, userResp.GetUserId()) + req.CallbackKind = &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + } + expectedContextInfo := contextInfoForUserOIDC(instance, "function/preuserinfo", userResp, userEmail, userPhone) + + targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) + + targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), []string{targetResp.GetId()}) + return userResp.GetUserId(), closeF +} + +func createSession(ctx context.Context, t *testing.T, instance *integration.Instance, userID string) *session.CreateSessionResponse { + sessionResp, err := instance.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: userID, + }, + }, + }, + }) + require.NoError(t, err) + return sessionResp +} + +func checkForSetMetadata(ctx context.Context, t *testing.T, instance *integration.Instance, userID string, metadataExpected []*metadata.Metadata) { + integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + assert.EventuallyWithT(t, func(ct *assert.CollectT) { + metadataResp, err := instance.Client.Mgmt.ListUserMetadata(ctx, &management.ListUserMetadataRequest{Id: userID}) + if !assert.NoError(ct, err) { + return + } + for _, dataExpected := range metadataExpected { + found := false + for _, dataCheck := range metadataResp.GetResult() { + if dataExpected.Key == dataCheck.Key { + found = true + if !assert.Equal(ct, dataExpected.Value, dataCheck.Value) { + return + } + } + } + if !assert.True(ct, found) { + return + } + } + }, retryDuration, tick) +} + +func getIDTokenClaimsFromCallbackURL(ctx context.Context, t *testing.T, instance *integration.Instance, clientID string, callbackURL *url.URL) map[string]any { + accessToken := callbackURL.Query().Get("access_token") + idToken := callbackURL.Query().Get("id_token") + + provider, err := instance.CreateRelyingParty(ctx, clientID, redirectURIImplicit, oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone) + require.NoError(t, err) + claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier()) + require.NoError(t, err) + return claims.Claims +} + +type CustomAccessTokenClaims struct { + oidc.TokenClaims + Added1 string `json:"added1,omitempty"` + Added2 string `json:"added2,omitempty"` + Added3 string `json:"added3,omitempty"` + Log []string `json:"urn:zitadel:iam:action:function/preaccesstoken:log,omitempty"` +} + +func getAccessTokenClaims(ctx context.Context, t *testing.T, instance *integration.Instance, callbackURL *url.URL) *CustomAccessTokenClaims { + accessToken := callbackURL.Query().Get("access_token") + + verifier := op.NewAccessTokenVerifier(instance.OIDCIssuer(), rp.NewRemoteKeySet(http.DefaultClient, instance.OIDCIssuer()+"/oauth/v2/keys")) + + claims, err := op.VerifyAccessToken[*CustomAccessTokenClaims](ctx, accessToken, verifier) + require.NoError(t, err) + return claims +} + +func contextInfoForUserOIDC(instance *integration.Instance, function string, userResp *user.AddHumanUserResponse, email, phone string) *oidc_api.ContextInfo { + return &oidc_api.ContextInfo{ + Function: function, + UserInfo: &oidc.UserInfo{ + Subject: userResp.GetUserId(), + }, + User: &query.User{ + ID: userResp.GetUserId(), + CreationDate: userResp.Details.ChangeDate.AsTime(), + ChangeDate: userResp.Details.ChangeDate.AsTime(), + ResourceOwner: instance.DefaultOrg.GetId(), + Sequence: userResp.Details.Sequence, + State: 1, + Username: email, + PreferredLoginName: email, + Human: &query.Human{ + FirstName: "Mickey", + LastName: "Mouse", + NickName: "Mickey", + DisplayName: "Mickey Mouse", + AvatarKey: "", + PreferredLanguage: language.Dutch, + Gender: 2, + Email: domain.EmailAddress(email), + IsEmailVerified: true, + Phone: domain.PhoneNumber(phone), + IsPhoneVerified: true, + PasswordChangeRequired: false, + PasswordChanged: time.Time{}, + MFAInitSkipped: time.Time{}, + }, + }, + UserMetadata: nil, + Org: &query.UserInfoOrg{ + ID: instance.DefaultOrg.GetId(), + Name: instance.DefaultOrg.GetName(), + PrimaryDomain: instance.DefaultOrg.GetPrimaryDomain(), + }, + UserGrants: nil, + Response: nil, + } +} + +func TestServer_ExecutionTargetPreAccessToken(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) + + client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2) + require.NoError(t, err) + + type want struct { + addedClaims *CustomAccessTokenClaims + addedLogClaims map[string][]string + setUserMetadata []*metadata.Metadata + } + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) + req *oidc_pb.CreateCallbackRequest + want want + wantErr bool + }{ + { + name: "append claim", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + AppendClaims: []*oidc_api.AppendClaim{ + {Key: "added1", Value: "value"}, + }, + } + return expectPreAccessTokenExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: &CustomAccessTokenClaims{ + Added1: "value", + }, + }, + wantErr: false, + }, + { + name: "append log claim", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + AppendLogClaims: []string{ + "addedLog", + }, + } + return expectPreAccessTokenExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: &CustomAccessTokenClaims{ + Log: []string{"addedLog"}, + }, + }, + wantErr: false, + }, + { + name: "set user metadata", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + } + return expectPreAccessTokenExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + setUserMetadata: []*metadata.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + }, + wantErr: false, + }, + { + name: "full usage", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + AppendLogClaims: []string{ + "addedLog1", + "addedLog2", + "addedLog3", + }, + AppendClaims: []*oidc_api.AppendClaim{ + {Key: "added1", Value: "value1"}, + {Key: "added2", Value: "value2"}, + {Key: "added3", Value: "value3"}, + }, + } + return expectPreAccessTokenExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: &CustomAccessTokenClaims{ + Added1: "value1", + Added2: "value2", + Added3: "value3", + Log: []string{"addedLog1", "addedLog2", "addedLog3"}, + }, + setUserMetadata: []*metadata.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID, closeF := tt.dep(isolatedIAMCtx, t, tt.req) + defer closeF() + + got, err := instance.Client.OIDCv2.CreateCallback(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + callbackUrl, err := url.Parse(strings.Replace(got.GetCallbackUrl(), "#", "?", 1)) + require.NoError(t, err) + claims := getAccessTokenClaims(tt.ctx, t, instance, callbackUrl) + + if tt.want.addedClaims != nil { + assert.Equal(t, tt.want.addedClaims.Added1, claims.Added1) + assert.Equal(t, tt.want.addedClaims.Added2, claims.Added2) + assert.Equal(t, tt.want.addedClaims.Added3, claims.Added3) + assert.Equal(t, tt.want.addedClaims.Log, claims.Log) + } + if len(tt.want.setUserMetadata) > 0 { + checkForSetMetadata(isolatedIAMCtx, t, instance, userID, tt.want.setUserMetadata) + } + + }) + } +} + +func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) { + userEmail := gofakeit.Email() + userPhone := "+41" + gofakeit.Phone() + userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone) + + sessionResp := createSession(ctx, t, instance, userResp.GetUserId()) + req.CallbackKind = &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + } + expectedContextInfo := contextInfoForUserOIDC(instance, "function/preaccesstoken", userResp, userEmail, userPhone) + + targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) + + targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preaccesstoken"), []string{targetResp.GetId()}) + return userResp.GetUserId(), closeF +} + +func TestServer_ExecutionTargetPreSAMLResponse(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) + + idpMetadata, err := instance.GetSAMLIDPMetadata() + require.NoError(t, err) + + acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1] + _, _, spMiddlewarePost := createSAMLApplication(isolatedIAMCtx, t, instance, idpMetadata, saml.HTTPPostBinding, false, false) + + type want struct { + addedAttributes map[string][]saml.AttributeValue + setUserMetadata []*metadata.Metadata + } + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) + req *saml_pb.CreateResponseRequest + want want + wantErr bool + }{ + { + name: "append attribute", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) { + response := &saml_api.ContextInfoResponse{ + AppendAttribute: []*saml_api.AppendAttribute{ + {Name: "added", NameFormat: "format", Value: []string{"value"}}, + }, + } + return expectPreSAMLResponseExecution(ctx, t, instance, req, response) + }, + req: &saml_pb.CreateResponseRequest{ + SamlRequestId: func() string { + _, samlRequestID, err := instance.CreateSAMLAuthRequest(spMiddlewarePost, instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) + require.NoError(t, err) + return samlRequestID + }(), + }, + want: want{ + addedAttributes: map[string][]saml.AttributeValue{ + "added": {saml.AttributeValue{Value: "value"}}, + }, + }, + wantErr: false, + }, + { + name: "set user metadata", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) { + response := &saml_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + } + return expectPreSAMLResponseExecution(ctx, t, instance, req, response) + }, + req: &saml_pb.CreateResponseRequest{ + SamlRequestId: func() string { + _, samlRequestID, err := instance.CreateSAMLAuthRequest(spMiddlewarePost, instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) + require.NoError(t, err) + return samlRequestID + }(), + }, + want: want{ + setUserMetadata: []*metadata.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + }, + wantErr: false, + }, + { + name: "set user metadata", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) { + response := &saml_api.ContextInfoResponse{ + AppendAttribute: []*saml_api.AppendAttribute{ + {Name: "added1", NameFormat: "format", Value: []string{"value1"}}, + {Name: "added2", NameFormat: "format", Value: []string{"value2"}}, + {Name: "added3", NameFormat: "format", Value: []string{"value3"}}, + }, + SetUserMetadata: []*domain.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + } + return expectPreSAMLResponseExecution(ctx, t, instance, req, response) + }, + req: &saml_pb.CreateResponseRequest{ + SamlRequestId: func() string { + _, samlRequestID, err := instance.CreateSAMLAuthRequest(spMiddlewarePost, instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) + require.NoError(t, err) + return samlRequestID + }(), + }, + want: want{ + addedAttributes: map[string][]saml.AttributeValue{ + "added1": {saml.AttributeValue{Value: "value1"}}, + "added2": {saml.AttributeValue{Value: "value2"}}, + "added3": {saml.AttributeValue{Value: "value3"}}, + }, + setUserMetadata: []*metadata.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID, closeF := tt.dep(isolatedIAMCtx, t, tt.req) + defer closeF() + + got, err := instance.Client.SAMLv2.CreateResponse(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + attributes := getSAMLResponseAttributes(t, got.GetPost().GetSamlResponse(), spMiddlewarePost) + for k, v := range tt.want.addedAttributes { + found := false + for _, attribute := range attributes { + if attribute.Name == k { + found = true + assert.Equal(t, v, attribute.Values) + } + } + if !assert.True(t, found) { + return + } + } + if len(tt.want.setUserMetadata) > 0 { + checkForSetMetadata(isolatedIAMCtx, t, instance, userID, tt.want.setUserMetadata) + } + }) + } +} + +func expectPreSAMLResponseExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *saml_pb.CreateResponseRequest, response *saml_api.ContextInfoResponse) (string, func()) { + userEmail := gofakeit.Email() + userPhone := "+41" + gofakeit.Phone() + userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone) + + sessionResp := createSession(ctx, t, instance, userResp.GetUserId()) + req.ResponseKind = &saml_pb.CreateResponseRequest_Session{ + Session: &saml_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + } + expectedContextInfo := contextInfoForUserSAML(instance, "function/presamlresponse", userResp, userEmail, userPhone) + + targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) + + targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("presamlresponse"), []string{targetResp.GetId()}) + + return userResp.GetUserId(), closeF +} + +func createSAMLSP(t *testing.T, idpMetadata *saml.EntityDescriptor, binding string) (string, *samlsp.Middleware) { + rootURL := "example." + gofakeit.DomainName() + spMiddleware, err := integration.CreateSAMLSP("https://"+rootURL, idpMetadata, binding) + require.NoError(t, err) + return rootURL, spMiddleware +} + +func createSAMLApplication(ctx context.Context, t *testing.T, instance *integration.Instance, idpMetadata *saml.EntityDescriptor, binding string, projectRoleCheck, hasProjectCheck bool) (string, string, *samlsp.Middleware) { + project, err := instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) + require.NoError(t, err) + rootURL, sp := createSAMLSP(t, idpMetadata, binding) + _, err = instance.CreateSAMLClient(ctx, project.GetId(), sp) + require.NoError(t, err) + return project.GetId(), rootURL, sp +} + +func getSAMLResponseAttributes(t *testing.T, samlResponse string, sp *samlsp.Middleware) []saml.Attribute { + data, err := base64.StdEncoding.DecodeString(samlResponse) + require.NoError(t, err) + sp.ServiceProvider.AllowIDPInitiated = true + assertion, err := sp.ServiceProvider.ParseXMLResponse(data, []string{}) + require.NoError(t, err) + return assertion.AttributeStatements[0].Attributes +} + +func contextInfoForUserSAML(instance *integration.Instance, function string, userResp *user.AddHumanUserResponse, email, phone string) *saml_api.ContextInfo { + return &saml_api.ContextInfo{ + Function: function, + User: &query.User{ + ID: userResp.GetUserId(), + CreationDate: userResp.Details.ChangeDate.AsTime(), + ChangeDate: userResp.Details.ChangeDate.AsTime(), + ResourceOwner: instance.DefaultOrg.GetId(), + Sequence: userResp.Details.Sequence, + State: 1, + Type: domain.UserTypeHuman, + Username: email, + PreferredLoginName: email, + LoginNames: []string{email}, + Human: &query.Human{ + FirstName: "Mickey", + LastName: "Mouse", + NickName: "Mickey", + DisplayName: "Mickey Mouse", + AvatarKey: "", + PreferredLanguage: language.Dutch, + Gender: 2, + Email: domain.EmailAddress(email), + IsEmailVerified: true, + Phone: domain.PhoneNumber(phone), + IsPhoneVerified: true, + PasswordChangeRequired: false, + PasswordChanged: time.Time{}, + MFAInitSkipped: time.Time{}, + }, + }, + UserGrants: nil, + Response: nil, + } +} diff --git a/internal/api/grpc/action/v2beta/integration_test/execution_test.go b/internal/api/grpc/action/v2beta/integration_test/execution_test.go new file mode 100644 index 0000000000..2199b9f454 --- /dev/null +++ b/internal/api/grpc/action/v2beta/integration_test/execution_test.go @@ -0,0 +1,565 @@ +//go:build integration + +package action_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" +) + +func TestServer_SetExecution_Request(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.NotExistingService/List", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/ListSessions", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "service, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Service{ + Service: "NotExistingService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "service, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Service{ + Service: "zitadel.session.v2beta.SessionService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "all, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_All{ + All: true, + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // We want to have the same response no matter how often we call the function + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req) + setDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) + + // cleanup to not impact other requests + instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func assertSetExecutionResponse(t *testing.T, creationDate, setDate time.Time, expectedSetDate bool, actualResp *action.SetExecutionResponse) { + if expectedSetDate { + if !setDate.IsZero() { + assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, setDate) + } else { + assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.SetDate) + } +} + +func TestServer_SetExecution_Response(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: "/zitadel.session.v2beta.NotExistingService/List", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/ListSessions", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "service, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Service{ + Service: "NotExistingService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "service, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Service{ + Service: "zitadel.session.v2beta.SessionService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "all, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_All{ + All: true, + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req) + setDate := time.Now().UTC() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) + + // cleanup to not impact other requests + instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func TestServer_SetExecution_Event(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_All{ + All: true, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "event, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Event{ + Event: "user.human.notexisting", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "event, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Event{ + Event: "user.human.added", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "group, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Group{ + Group: "user.notexisting", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "group, level 1, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Group{ + Group: "user", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "group, level 2, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Group{ + Group: "user.human", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "all, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_All{ + All: true, + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req) + setDate := time.Now().UTC() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) + + // cleanup to not impact other requests + instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func TestServer_SetExecution_Function(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "function, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Function{ + Function: &action.FunctionExecution{Name: "xxx"}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "function, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Function{ + Function: &action.FunctionExecution{Name: "presamlresponse"}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req) + setDate := time.Now().UTC() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) + + // cleanup to not impact other requests + instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} diff --git a/internal/api/grpc/action/v2beta/integration_test/query_test.go b/internal/api/grpc/action/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..5c59bee5d1 --- /dev/null +++ b/internal/api/grpc/action/v2beta/integration_test/query_test.go @@ -0,0 +1,793 @@ +//go:build integration + +package action_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" +) + +func TestServer_GetTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) error + req *action.GetTargetRequest + } + tests := []struct { + name string + args args + want *action.GetTargetResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.GetTargetRequest{}, + }, + wantErr: true, + }, + { + name: "not found", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.GetTargetRequest{Id: "notexisting"}, + }, + wantErr: true, + }, + { + name: "get, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + { + name: "get, async, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + { + name: "get, webhook interruptOnError, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + { + name: "get, call, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + { + name: "get, call interruptOnError, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + require.NoError(t, err) + } + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, 2*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV2beta.GetTarget(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, err, "Error: "+err.Error()) + return + } + assert.NoError(ttt, err) + assert.EqualExportedValues(ttt, tt.want, got) + }, retryDuration, tick, "timeout waiting for expected target result") + }) + } +} + +func TestServer_ListTargets(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + dep func(context.Context, *action.ListTargetsRequest, *action.ListTargetsResponse) + req *action.ListTargetsRequest + } + tests := []struct { + name string + args args + want *action.ListTargetsResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.ListTargetsRequest{}, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{ + {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{ + TargetIds: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Result: []*action.Target{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) + request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{ + TargetIds: []string{resp.GetId()}, + }, + } + + response.Result[0].Id = resp.GetId() + response.Result[0].Name = name + response.Result[0].CreationDate = resp.GetCreationDate() + response.Result[0].ChangeDate = resp.GetCreationDate() + response.Result[0].SigningKey = resp.GetSigningKey() + }, + req: &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{{}}, + }, + }, + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.Target{ + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + }, { + name: "list single name", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) + request.Filters[0].Filter = &action.TargetSearchFilter_TargetNameFilter{ + TargetNameFilter: &action.TargetNameFilter{ + TargetName: name, + }, + } + + response.Result[0].Id = resp.GetId() + response.Result[0].Name = name + response.Result[0].CreationDate = resp.GetCreationDate() + response.Result[0].ChangeDate = resp.GetCreationDate() + response.Result[0].SigningKey = resp.GetSigningKey() + }, + req: &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{{}}, + }, + }, + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.Target{ + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) { + name1 := gofakeit.Name() + name2 := gofakeit.Name() + name3 := gofakeit.Name() + resp1 := instance.CreateTarget(ctx, t, name1, "https://example.com", domain.TargetTypeWebhook, false) + resp2 := instance.CreateTarget(ctx, t, name2, "https://example.com", domain.TargetTypeCall, true) + resp3 := instance.CreateTarget(ctx, t, name3, "https://example.com", domain.TargetTypeAsync, false) + request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{ + TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + }, + } + + response.Result[2].Id = resp1.GetId() + response.Result[2].Name = name1 + response.Result[2].CreationDate = resp1.GetCreationDate() + response.Result[2].ChangeDate = resp1.GetCreationDate() + response.Result[2].SigningKey = resp1.GetSigningKey() + + response.Result[1].Id = resp2.GetId() + response.Result[1].Name = name2 + response.Result[1].CreationDate = resp2.GetCreationDate() + response.Result[1].ChangeDate = resp2.GetCreationDate() + response.Result[1].SigningKey = resp2.GetSigningKey() + + response.Result[0].Id = resp3.GetId() + response.Result[0].Name = name3 + response.Result[0].CreationDate = resp3.GetCreationDate() + response.Result[0].ChangeDate = resp3.GetCreationDate() + response.Result[0].SigningKey = resp3.GetSigningKey() + }, + req: &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{{}}, + }, + }, + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Result: []*action.Target{ + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(5 * time.Second), + }, + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.ActionV2beta.ListTargets(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Result, len(tt.want.Result)) { + for i := range tt.want.Result { + assert.EqualExportedValues(ttt, tt.want.Result[i], got.Result[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) { + assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit) + assert.Equal(t, expected.TotalResult, actual.TotalResult) +} + +func TestServer_ListExecutions(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) + + type args struct { + ctx context.Context + dep func(context.Context, *action.ListExecutionsRequest, *action.ListExecutionsResponse) + req *action.ListExecutionsRequest + } + tests := []struct { + name string + args args + want *action.ListExecutionsResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.ListExecutionsRequest{}, + }, + wantErr: true, + }, + { + name: "list request single condition", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0] + resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) + + // Set expected response with used values for SetExecution + response.Result[0].CreationDate = resp.GetSetDate() + response.Result[0].ChangeDate = resp.GetSetDate() + response.Result[0].Condition = cond + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/GetSession", + }, + }, + }, + }}, + }, + }, + }}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.Execution{ + { + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/GetSession", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + }, + }, + }, + { + name: "list request single target", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + target := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) + // add target as Filter to the request + request.Filters[0] = &action.ExecutionSearchFilter{ + Filter: &action.ExecutionSearchFilter_TargetFilter{ + TargetFilter: &action.TargetFilter{ + TargetId: target.GetId(), + }, + }, + } + cond := &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.management.v1.ManagementService/UpdateAction", + }, + }, + }, + } + resp := instance.SetExecution(ctx, t, cond, []string{target.GetId()}) + + response.Result[0].CreationDate = resp.GetSetDate() + response.Result[0].ChangeDate = resp.GetSetDate() + response.Result[0].Condition = cond + response.Result[0].Targets = []string{target.GetId()} + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{}}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.Execution{ + { + Condition: &action.Condition{}, + Targets: []string{""}, + }, + }, + }, + }, + { + name: "list multiple conditions", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + + request.Filters[0] = &action.ExecutionSearchFilter{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{ + {ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/GetSession", + }, + }, + }}, + {ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/CreateSession", + }, + }, + }}, + {ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/SetSession", + }, + }, + }}, + }, + }, + }, + } + + cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0] + resp1 := instance.SetExecution(ctx, t, cond1, []string{targetResp.GetId()}) + response.Result[2] = &action.Execution{ + CreationDate: resp1.GetSetDate(), + ChangeDate: resp1.GetSetDate(), + Condition: cond1, + Targets: []string{targetResp.GetId()}, + } + + cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1] + resp2 := instance.SetExecution(ctx, t, cond2, []string{targetResp.GetId()}) + response.Result[1] = &action.Execution{ + CreationDate: resp2.GetSetDate(), + ChangeDate: resp2.GetSetDate(), + Condition: cond2, + Targets: []string{targetResp.GetId()}, + } + + cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2] + resp3 := instance.SetExecution(ctx, t, cond3, []string{targetResp.GetId()}) + response.Result[0] = &action.Execution{ + CreationDate: resp3.GetSetDate(), + ChangeDate: resp3.GetSetDate(), + Condition: cond3, + Targets: []string{targetResp.GetId()}, + } + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{ + {}, + }, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Result: []*action.Execution{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple conditions all types", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + conditions := request.Filters[0].GetInConditionsFilter().GetConditions() + for i, cond := range conditions { + resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) + response.Result[(len(conditions)-1)-i] = &action.Execution{ + CreationDate: resp.GetSetDate(), + ChangeDate: resp.GetSetDate(), + Condition: cond, + Targets: []string{targetResp.GetId()}, + } + } + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{ + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "presamlresponse"}}}, + }, + }, + }, + }}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 10, + AppliedLimit: 100, + }, + Result: []*action.Execution{ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + }, + }, + }, + { + name: "list multiple conditions all types, sort id", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + conditions := request.Filters[0].GetInConditionsFilter().GetConditions() + for i, cond := range conditions { + resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) + response.Result[i] = &action.Execution{ + CreationDate: resp.GetSetDate(), + ChangeDate: resp.GetSetDate(), + Condition: cond, + Targets: []string{targetResp.GetId()}, + } + } + }, + req: &action.ListExecutionsRequest{ + SortingColumn: gu.Ptr(action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID), + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{ + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "presamlresponse"}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}, + }, + }, + }, + }}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 10, + AppliedLimit: 100, + }, + Result: []*action.Execution{ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.ActionV2beta.ListExecutions(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Result, len(tt.want.Result)) { + assert.EqualExportedValues(ttt, got.Result, tt.want.Result) + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func containExecution(t *assert.CollectT, executionList []*action.Execution, execution *action.Execution) bool { + for _, exec := range executionList { + if assert.EqualExportedValues(t, execution, exec) { + return true + } + } + return false +} diff --git a/internal/api/grpc/action/v2beta/integration_test/server_test.go b/internal/api/grpc/action/v2beta/integration_test/server_test.go new file mode 100644 index 0000000000..07ee051c63 --- /dev/null +++ b/internal/api/grpc/action/v2beta/integration_test/server_test.go @@ -0,0 +1,23 @@ +//go:build integration + +package action_test + +import ( + "context" + "os" + "testing" + "time" +) + +var ( + CTX context.Context +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + CTX = ctx + return m.Run() + }()) +} diff --git a/internal/api/grpc/action/v2beta/integration_test/target_test.go b/internal/api/grpc/action/v2beta/integration_test/target_test.go new file mode 100644 index 0000000000..8238d3146d --- /dev/null +++ b/internal/api/grpc/action/v2beta/integration_test/target_test.go @@ -0,0 +1,550 @@ +//go:build integration + +package action_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" +) + +func TestServer_CreateTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + type want struct { + id bool + creationDate bool + signingKey bool + } + alreadyExistingTargetName := gofakeit.AppName() + instance.CreateTarget(isolatedIAMOwnerCTX, t, alreadyExistingTargetName, "https://example.com", domain.TargetTypeAsync, false) + tests := []struct { + name string + ctx context.Context + req *action.CreateTargetRequest + want + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + }, + wantErr: true, + }, + { + name: "empty name", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: "", + }, + wantErr: true, + }, + { + name: "empty type", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + TargetType: nil, + }, + wantErr: true, + }, + { + name: "empty webhook url", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, + }, + }, + wantErr: true, + }, + { + name: "empty request response url", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{}, + }, + }, + wantErr: true, + }, + { + name: "empty timeout", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, + }, + Timeout: nil, + }, + wantErr: true, + }, + { + name: "async, already existing, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: alreadyExistingTargetName, + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }, + wantErr: true, + }, + { + name: "async, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + { + name: "webhook, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + { + name: "webhook, interrupt on error, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + { + name: "call, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + + { + name: "call, interruptOnError, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2beta.CreateTarget(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateTargetResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, tt.want.signingKey, got) + }) + } +} + +func assertCreateTargetResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate, expectedID, expectedSigningKey bool, actualResp *action.CreateTargetResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } + + if expectedID { + assert.NotEmpty(t, actualResp.GetId()) + } else { + assert.Nil(t, actualResp.Id) + } + + if expectedSigningKey { + assert.NotEmpty(t, actualResp.GetSigningKey()) + } else { + assert.Nil(t, actualResp.SigningKey) + } +} + +func TestServer_UpdateTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + req *action.UpdateTargetRequest + } + type want struct { + change bool + changeDate bool + signingKey bool + } + tests := []struct { + name string + prepare func(request *action.UpdateTargetRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.UpdateTargetRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *action.UpdateTargetRequest) { + request.Id = "notexisting" + return + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Endpoint: gu.Ptr("https://example.com"), + }, + }, + want: want{ + change: false, + changeDate: true, + signingKey: false, + }, + }, + { + name: "change name, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + { + name: "regenerate signingkey, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + ExpirationSigningKey: durationpb.New(0 * time.Second), + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: true, + }, + }, + { + name: "change type, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + TargetType: &action.UpdateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + { + name: "change url, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Endpoint: gu.Ptr("https://example.com/hooks/new"), + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + { + name: "change timeout, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Timeout: durationpb.New(20 * time.Second), + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + { + name: "change type async, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + TargetType: &action.UpdateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ActionV2beta.UpdateTarget(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateTargetResponse(t, creationDate, changeDate, tt.want.changeDate, tt.want.signingKey, got) + }) + } +} + +func assertUpdateTargetResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate, expectedSigningKey bool, actualResp *action.UpdateTargetResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } + + if expectedSigningKey { + assert.NotEmpty(t, actualResp.GetSigningKey()) + } else { + assert.Nil(t, actualResp.SigningKey) + } +} + +func TestServer_DeleteTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + tests := []struct { + name string + ctx context.Context + prepare func(request *action.DeleteTargetRequest) (time.Time, time.Time) + req *action.DeleteTargetRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.DeleteTargetRequest{ + Id: "notexisting", + }, + wantErr: true, + }, + { + name: "empty id", + ctx: iamOwnerCtx, + req: &action.DeleteTargetRequest{ + Id: "", + }, + wantErr: true, + }, + { + name: "delete target, not existing", + ctx: iamOwnerCtx, + req: &action.DeleteTargetRequest{ + Id: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete target", + ctx: iamOwnerCtx, + prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + return creationDate, time.Time{} + }, + req: &action.DeleteTargetRequest{}, + wantDeletionDate: true, + }, + { + name: "delete target, already removed", + ctx: iamOwnerCtx, + prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + instance.DeleteTarget(iamOwnerCtx, t, targetID) + return creationDate, time.Now().UTC() + }, + req: &action.DeleteTargetRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.ActionV2beta.DeleteTarget(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteTargetResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteTargetResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *action.DeleteTargetResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} diff --git a/internal/api/grpc/resources/action/v3alpha/query.go b/internal/api/grpc/action/v2beta/query.go similarity index 77% rename from internal/api/grpc/resources/action/v3alpha/query.go rename to internal/api/grpc/action/v2beta/query.go index 7cdedd8134..66bafa4e7d 100644 --- a/internal/api/grpc/resources/action/v3alpha/query.go +++ b/internal/api/grpc/action/v2beta/query.go @@ -5,14 +5,14 @@ import ( "strings" "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" - resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) const ( @@ -23,10 +23,6 @@ const ( ) func (s *Server) GetTarget(ctx context.Context, req *action.GetTargetRequest) (*action.GetTargetResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } - resp, err := s.query.GetTargetByID(ctx, req.GetId()) if err != nil { return nil, err @@ -45,11 +41,8 @@ type Context interface { GetOwner() InstanceContext } -func (s *Server) SearchTargets(ctx context.Context, req *action.SearchTargetsRequest) (*action.SearchTargetsResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } - queries, err := s.searchTargetsRequestToModel(req) +func (s *Server) ListTargets(ctx context.Context, req *action.ListTargetsRequest) (*action.ListTargetsResponse, error) { + queries, err := s.ListTargetsRequestToModel(req) if err != nil { return nil, err } @@ -57,17 +50,14 @@ func (s *Server) SearchTargets(ctx context.Context, req *action.SearchTargetsReq if err != nil { return nil, err } - return &action.SearchTargetsResponse{ - Result: targetsToPb(resp.Targets), - Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, resp.SearchResponse), + return &action.ListTargetsResponse{ + Result: targetsToPb(resp.Targets), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), }, nil } -func (s *Server) SearchExecutions(ctx context.Context, req *action.SearchExecutionsRequest) (*action.SearchExecutionsResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } - queries, err := s.searchExecutionsRequestToModel(req) +func (s *Server) ListExecutions(ctx context.Context, req *action.ListExecutionsRequest) (*action.ListExecutionsResponse, error) { + queries, err := s.ListExecutionsRequestToModel(req) if err != nil { return nil, err } @@ -75,45 +65,50 @@ func (s *Server) SearchExecutions(ctx context.Context, req *action.SearchExecuti if err != nil { return nil, err } - return &action.SearchExecutionsResponse{ - Result: executionsToPb(resp.Executions), - Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, resp.SearchResponse), + return &action.ListExecutionsResponse{ + Result: executionsToPb(resp.Executions), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), }, nil } -func targetsToPb(targets []*query.Target) []*action.GetTarget { - t := make([]*action.GetTarget, len(targets)) +func targetsToPb(targets []*query.Target) []*action.Target { + t := make([]*action.Target, len(targets)) for i, target := range targets { t[i] = targetToPb(target) } return t } -func targetToPb(t *query.Target) *action.GetTarget { - target := &action.GetTarget{ - Details: resource_object.DomainToDetailsPb(&t.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, t.ResourceOwner), - Config: &action.Target{ - Name: t.Name, - Timeout: durationpb.New(t.Timeout), - Endpoint: t.Endpoint, - }, +func targetToPb(t *query.Target) *action.Target { + target := &action.Target{ + Id: t.ObjectDetails.ID, + Name: t.Name, + Timeout: durationpb.New(t.Timeout), + Endpoint: t.Endpoint, SigningKey: t.SigningKey, } switch t.TargetType { case domain.TargetTypeWebhook: - target.Config.TargetType = &action.Target_RestWebhook{RestWebhook: &action.SetRESTWebhook{InterruptOnError: t.InterruptOnError}} + target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.RESTWebhook{InterruptOnError: t.InterruptOnError}} case domain.TargetTypeCall: - target.Config.TargetType = &action.Target_RestCall{RestCall: &action.SetRESTCall{InterruptOnError: t.InterruptOnError}} + target.TargetType = &action.Target_RestCall{RestCall: &action.RESTCall{InterruptOnError: t.InterruptOnError}} case domain.TargetTypeAsync: - target.Config.TargetType = &action.Target_RestAsync{RestAsync: &action.SetRESTAsync{}} + target.TargetType = &action.Target_RestAsync{RestAsync: &action.RESTAsync{}} default: - target.Config.TargetType = nil + target.TargetType = nil + } + + if !t.ObjectDetails.EventDate.IsZero() { + target.ChangeDate = timestamppb.New(t.ObjectDetails.EventDate) + } + if !t.ObjectDetails.CreationDate.IsZero() { + target.CreationDate = timestamppb.New(t.ObjectDetails.CreationDate) } return target } -func (s *Server) searchTargetsRequestToModel(req *action.SearchTargetsRequest) (*query.TargetSearchQueries, error) { - offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query) +func (s *Server) ListTargetsRequestToModel(req *action.ListTargetsRequest) (*query.TargetSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) if err != nil { return nil, err } @@ -155,7 +150,7 @@ func targetQueryToQuery(filter *action.TargetSearchFilter) (query.SearchQuery, e } func targetNameQueryToQuery(q *action.TargetNameFilter) (query.SearchQuery, error) { - return query.NewTargetNameSearchQuery(resource_object.TextMethodPbToQuery(q.Method), q.GetTargetName()) + return query.NewTargetNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetTargetName()) } func targetInTargetIdsQueryToQuery(q *action.InTargetIDsFilter) (query.SearchQuery, error) { @@ -210,8 +205,8 @@ func executionFieldNameToSortingColumn(field *action.ExecutionFieldName) query.C } } -func (s *Server) searchExecutionsRequestToModel(req *action.SearchExecutionsRequest) (*query.ExecutionSearchQueries, error) { - offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query) +func (s *Server) ListExecutionsRequestToModel(req *action.ListExecutionsRequest) (*query.ExecutionSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) if err != nil { return nil, err } @@ -247,12 +242,6 @@ func executionQueryToQuery(searchQuery *action.ExecutionSearchFilter) (query.Sea return inConditionsQueryToQuery(q.InConditionsFilter) case *action.ExecutionSearchFilter_ExecutionTypeFilter: return executionTypeToQuery(q.ExecutionTypeFilter) - case *action.ExecutionSearchFilter_IncludeFilter: - include, err := conditionToInclude(q.IncludeFilter.GetInclude()) - if err != nil { - return nil, err - } - return query.NewIncludeSearchQuery(include) case *action.ExecutionSearchFilter_TargetFilter: return query.NewTargetSearchQuery(q.TargetFilter.GetTargetId()) default: @@ -319,35 +308,38 @@ func conditionToID(q *action.Condition) (string, error) { } } -func executionsToPb(executions []*query.Execution) []*action.GetExecution { - e := make([]*action.GetExecution, len(executions)) +func executionsToPb(executions []*query.Execution) []*action.Execution { + e := make([]*action.Execution, len(executions)) for i, execution := range executions { e[i] = executionToPb(execution) } return e } -func executionToPb(e *query.Execution) *action.GetExecution { - targets := make([]*action.ExecutionTargetType, len(e.Targets)) +func executionToPb(e *query.Execution) *action.Execution { + targets := make([]string, len(e.Targets)) for i := range e.Targets { switch e.Targets[i].Type { - case domain.ExecutionTargetTypeInclude: - targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Include{Include: executionIDToCondition(e.Targets[i].Target)}} case domain.ExecutionTargetTypeTarget: - targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Target{Target: e.Targets[i].Target}} - case domain.ExecutionTargetTypeUnspecified: + targets[i] = e.Targets[i].Target + case domain.ExecutionTargetTypeInclude, domain.ExecutionTargetTypeUnspecified: continue default: continue } } - return &action.GetExecution{ - Details: resource_object.DomainToDetailsPb(&e.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, e.ResourceOwner), - Execution: &action.Execution{ - Targets: targets, - }, + exec := &action.Execution{ + Condition: executionIDToCondition(e.ID), + Targets: targets, } + if !e.ObjectDetails.EventDate.IsZero() { + exec.ChangeDate = timestamppb.New(e.ObjectDetails.EventDate) + } + if !e.ObjectDetails.CreationDate.IsZero() { + exec.CreationDate = timestamppb.New(e.ObjectDetails.CreationDate) + } + return exec } func executionIDToCondition(include string) *action.Condition { diff --git a/internal/api/grpc/resources/action/v3alpha/server.go b/internal/api/grpc/action/v2beta/server.go similarity index 66% rename from internal/api/grpc/resources/action/v3alpha/server.go rename to internal/api/grpc/action/v2beta/server.go index b80c60d668..ef0d8eb2ba 100644 --- a/internal/api/grpc/resources/action/v3alpha/server.go +++ b/internal/api/grpc/action/v2beta/server.go @@ -1,8 +1,6 @@ package action import ( - "context" - "google.golang.org/grpc" "github.com/zitadel/zitadel/internal/api/authz" @@ -10,14 +8,13 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) -var _ action.ZITADELActionsServer = (*Server)(nil) +var _ action.ActionServiceServer = (*Server)(nil) type Server struct { - action.UnimplementedZITADELActionsServer + action.UnimplementedActionServiceServer systemDefaults systemdefaults.SystemDefaults command *command.Commands query *query.Queries @@ -47,28 +44,21 @@ func CreateServer( } func (s *Server) RegisterServer(grpcServer *grpc.Server) { - action.RegisterZITADELActionsServer(grpcServer, s) + action.RegisterActionServiceServer(grpcServer, s) } func (s *Server) AppName() string { - return action.ZITADELActions_ServiceDesc.ServiceName + return action.ActionService_ServiceDesc.ServiceName } func (s *Server) MethodPrefix() string { - return action.ZITADELActions_ServiceDesc.ServiceName + return action.ActionService_ServiceDesc.ServiceName } func (s *Server) AuthMethods() authz.MethodMapping { - return action.ZITADELActions_AuthMethods + return action.ActionService_AuthMethods } func (s *Server) RegisterGateway() server.RegisterGatewayFunc { - return action.RegisterZITADELActionsHandler -} - -func checkActionsEnabled(ctx context.Context) error { - if authz.GetInstance(ctx).Features().Actions { - return nil - } - return zerrors.ThrowPreconditionFailed(nil, "ACTION-8o6pvqfjhs", "Errors.Action.NotEnabled") + return action.RegisterActionServiceHandler } diff --git a/internal/api/grpc/resources/action/v3alpha/target.go b/internal/api/grpc/action/v2beta/target.go similarity index 50% rename from internal/api/grpc/resources/action/v3alpha/target.go rename to internal/api/grpc/action/v2beta/target.go index 621b6677b7..26c88b9683 100644 --- a/internal/api/grpc/resources/action/v3alpha/target.go +++ b/internal/api/grpc/action/v2beta/target.go @@ -4,121 +4,122 @@ import ( "context" "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/authz" - resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } add := createTargetToCommand(req) instanceID := authz.GetInstance(ctx).InstanceID() - details, err := s.command.AddTarget(ctx, add, instanceID) + createdAt, err := s.command.AddTarget(ctx, add, instanceID) if err != nil { return nil, err } + var creationDate *timestamppb.Timestamp + if !createdAt.IsZero() { + creationDate = timestamppb.New(createdAt) + } return &action.CreateTargetResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), - SigningKey: add.SigningKey, + Id: add.AggregateID, + CreationDate: creationDate, + SigningKey: add.SigningKey, }, nil } -func (s *Server) PatchTarget(ctx context.Context, req *action.PatchTargetRequest) (*action.PatchTargetResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } +func (s *Server) UpdateTarget(ctx context.Context, req *action.UpdateTargetRequest) (*action.UpdateTargetResponse, error) { instanceID := authz.GetInstance(ctx).InstanceID() - patch := patchTargetToCommand(req) - details, err := s.command.ChangeTarget(ctx, patch, instanceID) + update := updateTargetToCommand(req) + changedAt, err := s.command.ChangeTarget(ctx, update, instanceID) if err != nil { return nil, err } - return &action.PatchTargetResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), - SigningKey: patch.SigningKey, + var changeDate *timestamppb.Timestamp + if !changedAt.IsZero() { + changeDate = timestamppb.New(changedAt) + } + return &action.UpdateTargetResponse{ + ChangeDate: changeDate, + SigningKey: update.SigningKey, }, nil } func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } instanceID := authz.GetInstance(ctx).InstanceID() - details, err := s.command.DeleteTarget(ctx, req.GetId(), instanceID) + deletedAt, err := s.command.DeleteTarget(ctx, req.GetId(), instanceID) if err != nil { return nil, err } + var deletionDate *timestamppb.Timestamp + if !deletedAt.IsZero() { + deletionDate = timestamppb.New(deletedAt) + } return &action.DeleteTargetResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + DeletionDate: deletionDate, }, nil } func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget { - reqTarget := req.GetTarget() var ( targetType domain.TargetType interruptOnError bool ) - switch t := reqTarget.GetTargetType().(type) { - case *action.Target_RestWebhook: + switch t := req.GetTargetType().(type) { + case *action.CreateTargetRequest_RestWebhook: targetType = domain.TargetTypeWebhook interruptOnError = t.RestWebhook.InterruptOnError - case *action.Target_RestCall: + case *action.CreateTargetRequest_RestCall: targetType = domain.TargetTypeCall interruptOnError = t.RestCall.InterruptOnError - case *action.Target_RestAsync: + case *action.CreateTargetRequest_RestAsync: targetType = domain.TargetTypeAsync } return &command.AddTarget{ - Name: reqTarget.GetName(), + Name: req.GetName(), TargetType: targetType, - Endpoint: reqTarget.GetEndpoint(), - Timeout: reqTarget.GetTimeout().AsDuration(), + Endpoint: req.GetEndpoint(), + Timeout: req.GetTimeout().AsDuration(), InterruptOnError: interruptOnError, } } -func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget { +func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarget { expirationSigningKey := false // TODO handle expiration, currently only immediate expiration is supported - if req.GetTarget().GetExpirationSigningKey() != nil { + if req.GetExpirationSigningKey() != nil { expirationSigningKey = true } - reqTarget := req.GetTarget() - if reqTarget == nil { + if req == nil { return nil } target := &command.ChangeTarget{ ObjectRoot: models.ObjectRoot{ AggregateID: req.GetId(), }, - Name: reqTarget.Name, - Endpoint: reqTarget.Endpoint, + Name: req.Name, + Endpoint: req.Endpoint, ExpirationSigningKey: expirationSigningKey, } - if reqTarget.TargetType != nil { - switch t := reqTarget.GetTargetType().(type) { - case *action.PatchTarget_RestWebhook: + if req.TargetType != nil { + switch t := req.GetTargetType().(type) { + case *action.UpdateTargetRequest_RestWebhook: target.TargetType = gu.Ptr(domain.TargetTypeWebhook) target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError) - case *action.PatchTarget_RestCall: + case *action.UpdateTargetRequest_RestCall: target.TargetType = gu.Ptr(domain.TargetTypeCall) target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError) - case *action.PatchTarget_RestAsync: + case *action.UpdateTargetRequest_RestAsync: target.TargetType = gu.Ptr(domain.TargetTypeAsync) target.InterruptOnError = gu.Ptr(false) } } - if reqTarget.Timeout != nil { - target.Timeout = gu.Ptr(reqTarget.GetTimeout().AsDuration()) + if req.Timeout != nil { + target.Timeout = gu.Ptr(req.GetTimeout().AsDuration()) } return target } diff --git a/internal/api/grpc/resources/action/v3alpha/target_test.go b/internal/api/grpc/action/v2beta/target_test.go similarity index 79% rename from internal/api/grpc/resources/action/v3alpha/target_test.go rename to internal/api/grpc/action/v2beta/target_test.go index f4e0d02e3b..b18ee52160 100644 --- a/internal/api/grpc/resources/action/v3alpha/target_test.go +++ b/internal/api/grpc/action/v2beta/target_test.go @@ -10,12 +10,12 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) func Test_createTargetToCommand(t *testing.T) { type args struct { - req *action.Target + req *action.CreateTargetRequest } tests := []struct { name string @@ -34,11 +34,11 @@ func Test_createTargetToCommand(t *testing.T) { }, { name: "all fields (webhook)", - args: args{&action.Target{ + args: args{&action.CreateTargetRequest{ Name: "target 1", Endpoint: "https://example.com/hooks/1", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{}, + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, }, Timeout: durationpb.New(10 * time.Second), }}, @@ -52,11 +52,11 @@ func Test_createTargetToCommand(t *testing.T) { }, { name: "all fields (async)", - args: args{&action.Target{ + args: args{&action.CreateTargetRequest{ Name: "target 1", Endpoint: "https://example.com/hooks/1", - TargetType: &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, + TargetType: &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, }, Timeout: durationpb.New(10 * time.Second), }}, @@ -70,11 +70,11 @@ func Test_createTargetToCommand(t *testing.T) { }, { name: "all fields (interrupting response)", - args: args{&action.Target{ + args: args{&action.CreateTargetRequest{ Name: "target 1", Endpoint: "https://example.com/hooks/1", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ InterruptOnError: true, }, }, @@ -91,7 +91,7 @@ func Test_createTargetToCommand(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := createTargetToCommand(&action.CreateTargetRequest{Target: tt.args.req}) + got := createTargetToCommand(tt.args.req) assert.Equal(t, tt.want, got) }) } @@ -99,7 +99,7 @@ func Test_createTargetToCommand(t *testing.T) { func Test_updateTargetToCommand(t *testing.T) { type args struct { - req *action.PatchTarget + req *action.UpdateTargetRequest } tests := []struct { name string @@ -113,7 +113,7 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields nil", - args: args{&action.PatchTarget{ + args: args{&action.UpdateTargetRequest{ Name: nil, TargetType: nil, Timeout: nil, @@ -128,7 +128,7 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields empty", - args: args{&action.PatchTarget{ + args: args{&action.UpdateTargetRequest{ Name: gu.Ptr(""), TargetType: nil, Timeout: durationpb.New(0), @@ -143,11 +143,11 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields (webhook)", - args: args{&action.PatchTarget{ + args: args{&action.UpdateTargetRequest{ Name: gu.Ptr("target 1"), Endpoint: gu.Ptr("https://example.com/hooks/1"), - TargetType: &action.PatchTarget_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ + TargetType: &action.UpdateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ InterruptOnError: false, }, }, @@ -163,11 +163,11 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields (webhook interrupt)", - args: args{&action.PatchTarget{ + args: args{&action.UpdateTargetRequest{ Name: gu.Ptr("target 1"), Endpoint: gu.Ptr("https://example.com/hooks/1"), - TargetType: &action.PatchTarget_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ + TargetType: &action.UpdateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ InterruptOnError: true, }, }, @@ -183,11 +183,11 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields (async)", - args: args{&action.PatchTarget{ + args: args{&action.UpdateTargetRequest{ Name: gu.Ptr("target 1"), Endpoint: gu.Ptr("https://example.com/hooks/1"), - TargetType: &action.PatchTarget_RestAsync{ - RestAsync: &action.SetRESTAsync{}, + TargetType: &action.UpdateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, }, Timeout: durationpb.New(10 * time.Second), }}, @@ -201,11 +201,11 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields (interrupting response)", - args: args{&action.PatchTarget{ + args: args{&action.UpdateTargetRequest{ Name: gu.Ptr("target 1"), Endpoint: gu.Ptr("https://example.com/hooks/1"), - TargetType: &action.PatchTarget_RestCall{ - RestCall: &action.SetRESTCall{ + TargetType: &action.UpdateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ InterruptOnError: true, }, }, @@ -222,7 +222,7 @@ func Test_updateTargetToCommand(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := patchTargetToCommand(&action.PatchTargetRequest{Target: tt.args.req}) + got := updateTargetToCommand(tt.args.req) assert.Equal(t, tt.want, got) }) } diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 7f7443fef7..5bbcab27cf 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -4,7 +4,8 @@ import ( "context" "encoding/base64" "fmt" - "io/ioutil" + "io" + "os" "strconv" "time" @@ -214,7 +215,7 @@ func (s *Server) transportDataFromFile(ctx context.Context, v1Transformation boo data = s3Data } if localInput != nil { - localData, err := ioutil.ReadFile(localInput.Path) + localData, err := os.ReadFile(localInput.Path) if err != nil { return nil, err } @@ -274,7 +275,7 @@ func getFileFromS3(ctx context.Context, input *admin_pb.ImportDataRequest_S3Inpu } defer object.Close() - return ioutil.ReadAll(object) + return io.ReadAll(object) } func getFileFromGCS(ctx context.Context, input *admin_pb.ImportDataRequest_GCSInput) (_ []byte, err error) { @@ -297,7 +298,7 @@ func getFileFromGCS(ctx context.Context, input *admin_pb.ImportDataRequest_GCSIn return nil, err } defer reader.Close() - return ioutil.ReadAll(reader) + return io.ReadAll(reader) } func importOrg1(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, ctxData authz.CtxData, org *admin_pb.DataOrg, success *admin_pb.ImportDataSuccess, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode crypto.Generator) (err error) { diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index 90e0ddc1d6..13f955fd81 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -69,7 +69,6 @@ func (s *Server) ListMyUserChanges(ctx context.Context, req *auth_pb.ListMyUserC } query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AllowTimeTravel(). Limit(limit). OrderDesc(). AwaitOpenTransactions(). diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go index 60cf569082..e146ac2db6 100644 --- a/internal/api/grpc/feature/v2/converter.go +++ b/internal/api/grpc/feature/v2/converter.go @@ -22,7 +22,6 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) (*command TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections, LegacyIntrospection: req.OidcLegacyIntrospection, UserSchema: req.UserSchema, - Actions: req.Actions, TokenExchange: req.OidcTokenExchange, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, @@ -41,7 +40,6 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent), @@ -62,7 +60,6 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) (*com LegacyIntrospection: req.OidcLegacyIntrospection, UserSchema: req.UserSchema, TokenExchange: req.OidcTokenExchange, - Actions: req.Actions, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), WebKey: req.WebKey, DebugOIDCParentError: req.DebugOidcParentError, @@ -83,7 +80,6 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), WebKey: featureSourceToFlagPb(&f.WebKey), DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), @@ -176,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 @@ -209,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: @@ -221,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/v2/converter_test.go b/internal/api/grpc/feature/v2/converter_test.go index 62cf701eec..f481e4f65a 100644 --- a/internal/api/grpc/feature/v2/converter_test.go +++ b/internal/api/grpc/feature/v2/converter_test.go @@ -23,7 +23,6 @@ func Test_systemFeaturesToCommand(t *testing.T) { OidcTriggerIntrospectionProjections: gu.Ptr(false), OidcLegacyIntrospection: nil, UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), ImprovedPerformance: nil, OidcSingleV1SessionTermination: gu.Ptr(true), @@ -37,7 +36,6 @@ func Test_systemFeaturesToCommand(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: nil, UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), TokenExchange: gu.Ptr(true), ImprovedPerformance: nil, OIDCSingleV1SessionTermination: gu.Ptr(true), @@ -74,10 +72,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: true, }, - Actions: query.FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, TokenExchange: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -132,10 +126,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Enabled: false, Source: feature_pb.Source_SOURCE_SYSTEM, }, - Actions: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_SYSTEM, - }, ImprovedPerformance: &feature_pb.ImprovedPerformanceFeatureFlag{ ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, Source: feature_pb.Source_SOURCE_SYSTEM, @@ -173,7 +163,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { OidcLegacyIntrospection: nil, UserSchema: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), - Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), DebugOidcParentError: gu.Ptr(true), @@ -191,7 +180,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { LegacyIntrospection: nil, UserSchema: gu.Ptr(true), TokenExchange: gu.Ptr(true), - Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), DebugOIDCParentError: gu.Ptr(true), @@ -231,10 +219,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelInstance, Value: true, }, - Actions: query.FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, TokenExchange: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -293,10 +277,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_INSTANCE, }, - Actions: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_INSTANCE, - }, OidcTokenExchange: &feature_pb.FeatureFlag{ Enabled: false, Source: feature_pb.Source_SOURCE_SYSTEM, diff --git a/internal/api/grpc/feature/v2/integration_test/feature_test.go b/internal/api/grpc/feature/v2/integration_test/feature_test.go index 2af4f642c4..f27b57ff8c 100644 --- a/internal/api/grpc/feature/v2/integration_test/feature_test.go +++ b/internal/api/grpc/feature/v2/integration_test/feature_test.go @@ -158,14 +158,6 @@ func TestServer_GetSystemFeatures(t *testing.T) { want *feature.GetSystemFeaturesResponse wantErr bool }{ - { - name: "permission error", - args: args{ - ctx: IamCTX, - req: &feature.GetSystemFeaturesRequest{}, - }, - wantErr: true, - }, { name: "nothing set", args: args{ @@ -219,7 +211,6 @@ func TestServer_GetSystemFeatures(t *testing.T) { assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections) assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection) assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema) - assertFeatureFlag(t, tt.want.Actions, got.Actions) }) } } @@ -349,14 +340,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { want *feature.GetInstanceFeaturesResponse wantErr bool }{ - { - name: "permission error", - args: args{ - ctx: OrgCTX, - req: &feature.GetInstanceFeaturesRequest{}, - }, - wantErr: true, - }, { name: "defaults, no inheritance", args: args{ @@ -390,10 +373,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - Actions: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, }, }, { @@ -403,7 +382,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { LoginDefaultOrg: gu.Ptr(true), OidcTriggerIntrospectionProjections: gu.Ptr(false), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), }) require.NoError(t, err) }, @@ -424,10 +402,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: true, Source: feature.Source_SOURCE_INSTANCE, }, - Actions: &feature.FeatureFlag{ - Enabled: true, - Source: feature.Source_SOURCE_INSTANCE, - }, }, }, { @@ -461,10 +435,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - Actions: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, }, }, } diff --git a/internal/api/grpc/feature/v2beta/converter.go b/internal/api/grpc/feature/v2beta/converter.go index 39f2284beb..9739e1c4c8 100644 --- a/internal/api/grpc/feature/v2beta/converter.go +++ b/internal/api/grpc/feature/v2beta/converter.go @@ -14,7 +14,6 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command. TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections, LegacyIntrospection: req.OidcLegacyIntrospection, UserSchema: req.UserSchema, - Actions: req.Actions, TokenExchange: req.OidcTokenExchange, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, @@ -29,7 +28,6 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), } @@ -42,7 +40,6 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm LegacyIntrospection: req.OidcLegacyIntrospection, UserSchema: req.UserSchema, TokenExchange: req.OidcTokenExchange, - Actions: req.Actions, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), WebKey: req.WebKey, DebugOIDCParentError: req.DebugOidcParentError, @@ -58,7 +55,6 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), WebKey: featureSourceToFlagPb(&f.WebKey), DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), @@ -113,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 @@ -146,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: @@ -158,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/feature/v2beta/converter_test.go b/internal/api/grpc/feature/v2beta/converter_test.go index 2896d8f77b..72d91b10d4 100644 --- a/internal/api/grpc/feature/v2beta/converter_test.go +++ b/internal/api/grpc/feature/v2beta/converter_test.go @@ -22,7 +22,6 @@ func Test_systemFeaturesToCommand(t *testing.T) { OidcTriggerIntrospectionProjections: gu.Ptr(false), OidcLegacyIntrospection: nil, UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), ImprovedPerformance: nil, OidcSingleV1SessionTermination: gu.Ptr(true), @@ -32,7 +31,6 @@ func Test_systemFeaturesToCommand(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: nil, UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), TokenExchange: gu.Ptr(true), ImprovedPerformance: nil, OIDCSingleV1SessionTermination: gu.Ptr(true), @@ -64,10 +62,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: true, }, - Actions: query.FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, TokenExchange: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -107,10 +101,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Enabled: false, Source: feature_pb.Source_SOURCE_SYSTEM, }, - Actions: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_SYSTEM, - }, ImprovedPerformance: &feature_pb.ImprovedPerformanceFeatureFlag{ ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, Source: feature_pb.Source_SOURCE_SYSTEM, @@ -131,7 +121,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { OidcLegacyIntrospection: nil, UserSchema: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), - Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), OidcSingleV1SessionTermination: gu.Ptr(true), @@ -142,7 +131,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { LegacyIntrospection: nil, UserSchema: gu.Ptr(true), TokenExchange: gu.Ptr(true), - Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), OIDCSingleV1SessionTermination: gu.Ptr(true), @@ -174,10 +162,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelInstance, Value: true, }, - Actions: query.FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, TokenExchange: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -217,10 +201,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_INSTANCE, }, - Actions: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_INSTANCE, - }, OidcTokenExchange: &feature_pb.FeatureFlag{ Enabled: false, Source: feature_pb.Source_SOURCE_SYSTEM, diff --git a/internal/api/grpc/feature/v2beta/integration_test/feature_test.go b/internal/api/grpc/feature/v2beta/integration_test/feature_test.go index 69e05352d0..cbd9f5f939 100644 --- a/internal/api/grpc/feature/v2beta/integration_test/feature_test.go +++ b/internal/api/grpc/feature/v2beta/integration_test/feature_test.go @@ -202,10 +202,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - Actions: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, }, }, { @@ -215,7 +211,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { LoginDefaultOrg: gu.Ptr(true), OidcTriggerIntrospectionProjections: gu.Ptr(false), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), }) require.NoError(t, err) }, @@ -236,10 +231,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: true, Source: feature.Source_SOURCE_INSTANCE, }, - Actions: &feature.FeatureFlag{ - Enabled: true, - Source: feature.Source_SOURCE_INSTANCE, - }, }, }, { @@ -273,10 +264,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - Actions: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, }, }, } diff --git a/internal/api/grpc/filter/v2beta/converter.go b/internal/api/grpc/filter/v2beta/converter.go new file mode 100644 index 0000000000..e34f9dd9d7 --- /dev/null +++ b/internal/api/grpc/filter/v2beta/converter.go @@ -0,0 +1,56 @@ +package filter + +import ( + "fmt" + + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" +) + +func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison { + switch method { + case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS: + return query.TextEquals + case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE: + return query.TextEqualsIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH: + return query.TextStartsWith + case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE: + return query.TextStartsWithIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS: + return query.TextContains + case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE: + return query.TextContainsIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH: + return query.TextEndsWith + case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE: + return query.TextEndsWithIgnoreCase + default: + return -1 + } +} + +func PaginationPbToQuery(defaults systemdefaults.SystemDefaults, query *filter.PaginationRequest) (offset, limit uint64, asc bool, err error) { + limit = defaults.DefaultQueryLimit + if query == nil { + return 0, limit, asc, nil + } + offset = query.Offset + asc = query.Asc + if defaults.MaxQueryLimit > 0 && uint64(query.Limit) > defaults.MaxQueryLimit { + return 0, 0, false, zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", query.Limit, defaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded") + } + if query.Limit > 0 { + limit = uint64(query.Limit) + } + return offset, limit, asc, nil +} + +func QueryToPaginationPb(request query.SearchRequest, response query.SearchResponse) *filter.PaginationResponse { + return &filter.PaginationResponse{ + AppliedLimit: request.Limit, + TotalResult: response.Count, + } +} diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index abc179a763..a6a934160a 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -50,7 +50,6 @@ func (s *Server) ListOrgChanges(ctx context.Context, req *mgmt_pb.ListOrgChanges } query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AllowTimeTravel(). Limit(limit). OrderDesc(). AwaitOpenTransactions(). diff --git a/internal/api/grpc/management/project.go b/internal/api/grpc/management/project.go index 00ccbd215c..52b6b10e9a 100644 --- a/internal/api/grpc/management/project.go +++ b/internal/api/grpc/management/project.go @@ -70,7 +70,6 @@ func (s *Server) ListProjectGrantChanges(ctx context.Context, req *mgmt_pb.ListP } query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AllowTimeTravel(). Limit(limit). OrderDesc(). ResourceOwner(authz.GetCtxData(ctx).OrgID). @@ -152,7 +151,6 @@ func (s *Server) ListProjectChanges(ctx context.Context, req *mgmt_pb.ListProjec } query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AllowTimeTravel(). Limit(limit). AwaitOpenTransactions(). OrderDesc(). diff --git a/internal/api/grpc/management/project_application.go b/internal/api/grpc/management/project_application.go index 4b65808776..3a0e1d5f92 100644 --- a/internal/api/grpc/management/project_application.go +++ b/internal/api/grpc/management/project_application.go @@ -52,7 +52,6 @@ func (s *Server) ListAppChanges(ctx context.Context, req *mgmt_pb.ListAppChanges } query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AllowTimeTravel(). Limit(limit). AwaitOpenTransactions(). OrderDesc(). diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 17bca58993..b876999584 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -92,7 +92,6 @@ func (s *Server) ListUserChanges(ctx context.Context, req *mgmt_pb.ListUserChang } query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AllowTimeTravel(). Limit(limit). AwaitOpenTransactions(). OrderDesc(). diff --git a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go index 1eb031bd6d..64334bd8b1 100644 --- a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go @@ -77,19 +77,22 @@ func TestServer_GetAuthRequest(t *testing.T) { now, authRequestID, err := tt.dep() require.NoError(t, err) - got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{ - AuthRequestId: authRequestID, - }) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - authRequest := got.GetAuthRequest() - assert.NotNil(t, authRequest) - assert.Equal(t, authRequestID, authRequest.GetId()) - assert.WithinRange(t, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second)) - assert.Contains(t, authRequest.GetScope(), "openid") + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{ + AuthRequestId: authRequestID, + }) + if tt.wantErr { + assert.Error(ttt, err) + return + } + assert.NoError(ttt, err) + authRequest := got.GetAuthRequest() + assert.NotNil(ttt, authRequest) + assert.Equal(ttt, authRequestID, authRequest.GetId()) + assert.WithinRange(ttt, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second)) + assert.Contains(ttt, authRequest.GetScope(), "openid") + }, retryDuration, tick) }) } } @@ -104,13 +107,11 @@ func TestServer_CreateCallback(t *testing.T) { sessionResp := createSession(t, CTX, Instance.Users[integration.UserTypeOrgOwner].ID) tests := []struct { - name string - ctx context.Context - req *oidc_pb.CreateCallbackRequest - AuthError string - want *oidc_pb.CreateCallbackResponse - wantURL *url.URL - wantErr bool + name string + ctx context.Context + req *oidc_pb.CreateCallbackRequest + want *oidc_pb.CreateCallbackResponse + wantErr bool }{ { name: "Not found", @@ -673,21 +674,24 @@ func TestServer_GetDeviceAuthorizationRequest(t *testing.T) { deviceAuth, err := tt.dep() require.NoError(t, err) - got, err := Client.GetDeviceAuthorizationRequest(tt.ctx, &oidc_pb.GetDeviceAuthorizationRequestRequest{ - UserCode: deviceAuth.UserCode, - }) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - authRequest := got.GetDeviceAuthorizationRequest() - assert.NotNil(t, authRequest) - assert.NotEmpty(t, authRequest.GetId()) - assert.Equal(t, client.GetClientId(), authRequest.GetClientId()) - assert.Contains(t, authRequest.GetScope(), "openid") - assert.NotEmpty(t, authRequest.GetAppName()) - assert.NotEmpty(t, authRequest.GetProjectName()) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.GetDeviceAuthorizationRequest(tt.ctx, &oidc_pb.GetDeviceAuthorizationRequestRequest{ + UserCode: deviceAuth.UserCode, + }) + if tt.wantErr { + assert.Error(ttt, err) + return + } + assert.NoError(ttt, err) + authRequest := got.GetDeviceAuthorizationRequest() + assert.NotNil(ttt, authRequest) + assert.NotEmpty(ttt, authRequest.GetId()) + assert.Equal(ttt, client.GetClientId(), authRequest.GetClientId()) + assert.Contains(ttt, authRequest.GetScope(), "openid") + assert.NotEmpty(ttt, authRequest.GetAppName()) + assert.NotEmpty(ttt, authRequest.GetProjectName()) + }, retryDuration, tick) }) } } diff --git a/internal/api/grpc/oidc/v2/oidc.go b/internal/api/grpc/oidc/v2/oidc.go index 73fc995be2..8612d11558 100644 --- a/internal/api/grpc/oidc/v2/oidc.go +++ b/internal/api/grpc/oidc/v2/oidc.go @@ -10,7 +10,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - "github.com/zitadel/zitadel/internal/api/http" + http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -158,7 +158,11 @@ func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID str return nil, err } authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar} - ctx = op.ContextWithIssuer(ctx, http.DomainContext(ctx).Origin()) + issuer := authReq.Issuer + if issuer == "" { + issuer = http_utils.DomainContext(ctx).Origin() + } + ctx = op.ContextWithIssuer(ctx, issuer) var callback string if aar.ResponseType == domain.OIDCResponseTypeCode { callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op.Provider()) diff --git a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go index 1d2a6d2671..d7d746e2d0 100644 --- a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go @@ -76,19 +76,22 @@ func TestServer_GetAuthRequest(t *testing.T) { now, authRequestID, err := tt.dep() require.NoError(t, err) - got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{ - AuthRequestId: authRequestID, - }) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - authRequest := got.GetAuthRequest() - assert.NotNil(t, authRequest) - assert.Equal(t, authRequestID, authRequest.GetId()) - assert.WithinRange(t, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second)) - assert.Contains(t, authRequest.GetScope(), "openid") + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{ + AuthRequestId: authRequestID, + }) + if tt.wantErr { + assert.Error(ttt, err) + return + } + assert.NoError(ttt, err) + authRequest := got.GetAuthRequest() + assert.NotNil(ttt, authRequest) + assert.Equal(ttt, authRequestID, authRequest.GetId()) + assert.WithinRange(ttt, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second)) + assert.Contains(ttt, authRequest.GetScope(), "openid") + }, retryDuration, tick) }) } } diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go deleted file mode 100644 index 7aff6afb3f..0000000000 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go +++ /dev/null @@ -1,410 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "reflect" - "testing" - "time" - - "github.com/brianvoe/gofakeit/v6" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - - "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" -) - -func TestServer_ExecutionTarget(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - - fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget" - - tests := []struct { - name string - ctx context.Context - dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) (func(), error) - clean func(context.Context) - req *action.GetTargetRequest - want *action.GetTargetResponse - wantErr bool - }{ - { - name: "GetTarget, request and response, ok", - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) { - - orgID := instance.DefaultOrg.Id - projectID := "" - userID := instance.Users.Get(integration.UserTypeIAMOwner).ID - - // create target for target changes - targetCreatedName := gofakeit.Name() - targetCreatedURL := "https://nonexistent" - - targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) - - // request received by target - wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request} - changedRequest := &action.GetTargetRequest{Id: targetCreated.GetDetails().GetId()} - // replace original request with different targetID - urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusOK, changedRequest) - - targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, false) - - waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetDetails().GetId())) - - // expected response from the GetTarget - expectedResponse := &action.GetTargetResponse{ - Target: &action.GetTarget{ - Config: &action.Target{ - Name: targetCreatedName, - Endpoint: targetCreatedURL, - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - Details: targetCreated.GetDetails(), - }, - } - // has to be set separately because of the pointers - response.Target = &action.GetTarget{ - Details: targetCreated.GetDetails(), - Config: &action.Target{ - Name: targetCreatedName, - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - Endpoint: targetCreatedURL, - }, - } - - // content for partial update - changedResponse := &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Id: targetCreated.GetDetails().GetId(), - }, - }, - } - - // response received by target - wantResponse := &middleware.ContextInfoResponse{ - FullMethod: fullMethod, - InstanceID: instance.ID(), - OrgID: orgID, - ProjectID: projectID, - UserID: userID, - Request: changedRequest, - Response: expectedResponse, - } - // after request with different targetID, return changed response - targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusOK, changedResponse) - - targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, false) - waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetDetails().GetId())) - return func() { - closeRequest() - closeResponse() - }, nil - }, - clean: func(ctx context.Context) { - instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) - instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) - }, - req: &action.GetTargetRequest{ - Id: "something", - }, - want: &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Id: "changed", - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - }, - { - name: "GetTarget, request, interrupt", - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) { - - fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget" - orgID := instance.DefaultOrg.Id - projectID := "" - userID := instance.Users.Get(integration.UserTypeIAMOwner).ID - - // request received by target - wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request} - urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusInternalServerError, &action.GetTargetRequest{Id: "notchanged"}) - - targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, true) - waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetDetails().GetId())) - // GetTarget with used target - request.Id = targetRequest.GetDetails().GetId() - return func() { - closeRequest() - }, nil - }, - clean: func(ctx context.Context) { - instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) - }, - req: &action.GetTargetRequest{}, - wantErr: true, - }, - { - name: "GetTarget, response, interrupt", - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) { - - fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget" - orgID := instance.DefaultOrg.Id - projectID := "" - userID := instance.Users.Get(integration.UserTypeIAMOwner).ID - - // create target for target changes - targetCreatedName := gofakeit.Name() - targetCreatedURL := "https://nonexistent" - - targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) - - // GetTarget with used target - request.Id = targetCreated.GetDetails().GetId() - - // expected response from the GetTarget - expectedResponse := &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: targetCreated.GetDetails(), - Config: &action.Target{ - Name: targetCreatedName, - Endpoint: targetCreatedURL, - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - } - // content for partial update - changedResponse := &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Id: "changed", - }, - }, - } - - // response received by target - wantResponse := &middleware.ContextInfoResponse{ - FullMethod: fullMethod, - InstanceID: instance.ID(), - OrgID: orgID, - ProjectID: projectID, - UserID: userID, - Request: request, - Response: expectedResponse, - } - // after request with different targetID, return changed response - targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusInternalServerError, changedResponse) - - targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, true) - waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetDetails().GetId())) - return func() { - closeResponse() - }, nil - }, - clean: func(ctx context.Context) { - instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) - }, - req: &action.GetTargetRequest{}, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.dep != nil { - close, err := tt.dep(tt.ctx, tt.req, tt.want) - require.NoError(t, err) - defer close() - } - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := instance.Client.ActionV3Alpha.GetTarget(tt.ctx, tt.req) - if tt.wantErr { - require.Error(ttt, err) - return - } - require.NoError(ttt, err) - - integration.AssertResourceDetails(ttt, tt.want.GetTarget().GetDetails(), got.GetTarget().GetDetails()) - tt.want.Target.Details = got.GetTarget().GetDetails() - assert.EqualExportedValues(ttt, tt.want.GetTarget().GetConfig(), got.GetTarget().GetConfig()) - - }, retryDuration, tick, "timeout waiting for expected execution result") - - if tt.clean != nil { - tt.clean(tt.ctx) - } - }) - } -} - -func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *integration.Instance, condition *action.Condition, targets []*action.ExecutionTargetType) { - instance.SetExecution(ctx, t, condition, targets) - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := instance.Client.ActionV3Alpha.SearchExecutions(ctx, &action.SearchExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{ - {Filter: &action.ExecutionSearchFilter_InConditionsFilter{ - InConditionsFilter: &action.InConditionsFilter{Conditions: []*action.Condition{condition}}, - }}, - }, - }) - if !assert.NoError(ttt, err) { - return - } - if !assert.Len(ttt, got.GetResult(), 1) { - return - } - gotTargets := got.GetResult()[0].GetExecution().GetTargets() - // always first check length, otherwise its failed anyway - if assert.Len(ttt, gotTargets, len(targets)) { - for i := range targets { - assert.EqualExportedValues(ttt, targets[i].GetType(), gotTargets[i].GetType()) - } - } - }, retryDuration, tick, "timeout waiting for expected execution result") - return -} - -func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Instance, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { - resp := instance.CreateTarget(ctx, t, "", endpoint, ty, interrupt) - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := instance.Client.ActionV3Alpha.SearchTargets(ctx, &action.SearchTargetsRequest{ - Filters: []*action.TargetSearchFilter{ - {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ - InTargetIdsFilter: &action.InTargetIDsFilter{TargetIds: []string{resp.GetDetails().GetId()}}, - }}, - }, - }) - if !assert.NoError(ttt, err) { - return - } - if !assert.Len(ttt, got.GetResult(), 1) { - return - } - config := got.GetResult()[0].GetConfig() - assert.Equal(ttt, config.GetEndpoint(), endpoint) - switch ty { - case domain.TargetTypeWebhook: - if !assert.NotNil(ttt, config.GetRestWebhook()) { - return - } - assert.Equal(ttt, interrupt, config.GetRestWebhook().GetInterruptOnError()) - case domain.TargetTypeAsync: - assert.NotNil(ttt, config.GetRestAsync()) - case domain.TargetTypeCall: - if !assert.NotNil(ttt, config.GetRestCall()) { - return - } - assert.Equal(ttt, interrupt, config.GetRestCall().GetInterruptOnError()) - } - }, retryDuration, tick, "timeout waiting for expected execution result") - return resp -} - -func conditionRequestFullMethod(fullMethod string) *action.Condition { - return &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: fullMethod, - }, - }, - }, - } -} - -func conditionResponseFullMethod(fullMethod string) *action.Condition { - return &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: fullMethod, - }, - }, - }, - } -} - -func testServerCall( - reqBody interface{}, - sleep time.Duration, - statusCode int, - respBody interface{}, -) (string, func()) { - handler := func(w http.ResponseWriter, r *http.Request) { - data, err := json.Marshal(reqBody) - if err != nil { - http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError) - return - } - - sentBody, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError) - return - } - if !reflect.DeepEqual(data, sentBody) { - http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError) - return - } - if statusCode != http.StatusOK { - http.Error(w, "error, statusCode", statusCode) - return - } - - time.Sleep(sleep) - - w.Header().Set("Content-Type", "application/json") - resp, err := json.Marshal(respBody) - if err != nil { - http.Error(w, "error", http.StatusInternalServerError) - return - } - if _, err := io.WriteString(w, string(resp)); err != nil { - http.Error(w, "error", http.StatusInternalServerError) - return - } - } - - server := httptest.NewServer(http.HandlerFunc(handler)) - - return server.URL, server.Close -} diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go deleted file mode 100644 index b56efd6b99..0000000000 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go +++ /dev/null @@ -1,812 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" -) - -func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType { - return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Target{Target: id}}} -} - -func executionTargetsSingleInclude(include *action.Condition) []*action.ExecutionTargetType { - return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Include{Include: include}}} -} - -func TestServer_SetExecution_Request(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{All: true}, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{}, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "method, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.NotExistingService/List", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "service, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "NotExistingService", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "service, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "all, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{ - All: true, - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // We want to have the same response no matter how often we call the function - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertResourceDetails(t, tt.want.Details, got.Details) - - // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_SetExecution_Request_Include(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - executionCond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{ - All: true, - }, - }, - }, - } - instance.SetExecution(isolatedIAMOwnerCTX, t, - executionCond, - executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - ) - - circularExecutionService := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", - }, - }, - }, - } - instance.SetExecution(isolatedIAMOwnerCTX, t, - circularExecutionService, - executionTargetsSingleInclude(executionCond), - ) - circularExecutionMethod := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", - }, - }, - }, - } - instance.SetExecution(isolatedIAMOwnerCTX, t, - circularExecutionMethod, - executionTargetsSingleInclude(circularExecutionService), - ) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "method, circular error", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: circularExecutionService, - Execution: &action.Execution{ - Targets: executionTargetsSingleInclude(circularExecutionMethod), - }, - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleInclude(executionCond), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "service, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleInclude(executionCond), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // We want to have the same response no matter how often we call the function - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertResourceDetails(t, tt.want.Details, got.Details) - - // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_SetExecution_Response(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{All: true}, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{}, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "method, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2beta.NotExistingService/List", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "service, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Service{ - Service: "NotExistingService", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "service, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "all, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{ - All: true, - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // We want to have the same response no matter how often we call the function - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertResourceDetails(t, tt.want.Details, got.Details) - - // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_SetExecution_Event(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_All{ - All: true, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{}, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - /* - //TODO event existing check - - { - name: "event, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Event{ - Event: "xxx", - }, - }, - }, - }, - Targets: []string{targetResp.GetId()}, - }, - wantErr: true, - }, - */ - { - name: "event, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Event{ - Event: "xxx", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - /* - // TODO: - - { - name: "group, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Group{ - Group: "xxx", - }, - }, - }, - }, - Targets: []string{targetResp.GetId()}, - }, - wantErr: true, - }, - */ - { - name: "group, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Group{ - Group: "xxx", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "all, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_All{ - All: true, - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // We want to have the same response no matter how often we call the function - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertResourceDetails(t, tt.want.Details, got.Details) - - // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_SetExecution_Function(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{All: true}, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{}, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "function, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Function{ - Function: &action.FunctionExecution{Name: "xxx"}, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "function, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Function{ - Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // We want to have the same response no matter how often we call the function - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertResourceDetails(t, tt.want.Details, got.Details) - - // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/query_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/query_test.go deleted file mode 100644 index aa748ac4d8..0000000000 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/query_test.go +++ /dev/null @@ -1,905 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/brianvoe/gofakeit/v6" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" -) - -func TestServer_GetTarget(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - type args struct { - ctx context.Context - dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) error - req *action.GetTargetRequest - } - tests := []struct { - name string - args args - want *action.GetTargetResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.GetTargetRequest{}, - }, - wantErr: true, - }, - { - name: "not found", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.GetTargetRequest{Id: "notexisting"}, - }, - wantErr: true, - }, - { - name: "get, ok", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) - request.Id = resp.GetDetails().GetId() - response.Target.Config.Name = name - response.Target.Details = resp.GetDetails() - response.Target.SigningKey = resp.GetSigningKey() - return nil - }, - req: &action.GetTargetRequest{}, - }, - want: &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - { - name: "get, async, ok", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false) - request.Id = resp.GetDetails().GetId() - response.Target.Config.Name = name - response.Target.Details = resp.GetDetails() - response.Target.SigningKey = resp.GetSigningKey() - return nil - }, - req: &action.GetTargetRequest{}, - }, - want: &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - { - name: "get, webhook interruptOnError, ok", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true) - request.Id = resp.GetDetails().GetId() - response.Target.Config.Name = name - response.Target.Details = resp.GetDetails() - response.Target.SigningKey = resp.GetSigningKey() - return nil - }, - req: &action.GetTargetRequest{}, - }, - want: &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - { - name: "get, call, ok", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false) - request.Id = resp.GetDetails().GetId() - response.Target.Config.Name = name - response.Target.Details = resp.GetDetails() - response.Target.SigningKey = resp.GetSigningKey() - return nil - }, - req: &action.GetTargetRequest{}, - }, - want: &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - { - name: "get, call interruptOnError, ok", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true) - request.Id = resp.GetDetails().GetId() - response.Target.Config.Name = name - response.Target.Details = resp.GetDetails() - response.Target.SigningKey = resp.GetSigningKey() - return nil - }, - req: &action.GetTargetRequest{}, - }, - want: &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.dep != nil { - err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) - require.NoError(t, err) - } - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, 2*time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := instance.Client.ActionV3Alpha.GetTarget(tt.args.ctx, tt.args.req) - if tt.wantErr { - assert.Error(ttt, err, "Error: "+err.Error()) - return - } - if !assert.NoError(ttt, err) { - return - } - - wantTarget := tt.want.GetTarget() - gotTarget := got.GetTarget() - integration.AssertResourceDetails(ttt, wantTarget.GetDetails(), gotTarget.GetDetails()) - assert.EqualExportedValues(ttt, wantTarget.GetConfig(), gotTarget.GetConfig()) - assert.Equal(ttt, wantTarget.GetSigningKey(), gotTarget.GetSigningKey()) - }, retryDuration, tick, "timeout waiting for expected target result") - }) - } -} - -func TestServer_ListTargets(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - type args struct { - ctx context.Context - dep func(context.Context, *action.SearchTargetsRequest, *action.SearchTargetsResponse) error - req *action.SearchTargetsRequest - } - tests := []struct { - name string - args args - want *action.SearchTargetsResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SearchTargetsRequest{}, - }, - wantErr: true, - }, - { - name: "list, not found", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.SearchTargetsRequest{ - Filters: []*action.TargetSearchFilter{ - {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ - InTargetIdsFilter: &action.InTargetIDsFilter{ - TargetIds: []string{"notfound"}, - }, - }, - }, - }, - }, - }, - want: &action.SearchTargetsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 0, - AppliedLimit: 100, - }, - Result: []*action.GetTarget{}, - }, - }, - { - name: "list single id", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) - request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{ - InTargetIdsFilter: &action.InTargetIDsFilter{ - TargetIds: []string{resp.GetDetails().GetId()}, - }, - } - response.Details.Timestamp = resp.GetDetails().GetChanged() - - response.Result[0].Details = resp.GetDetails() - response.Result[0].Config.Name = name - return nil - }, - req: &action.SearchTargetsRequest{ - Filters: []*action.TargetSearchFilter{{}}, - }, - }, - want: &action.SearchTargetsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 1, - AppliedLimit: 100, - }, - Result: []*action.GetTarget{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - }, { - name: "list single name", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) - request.Filters[0].Filter = &action.TargetSearchFilter_TargetNameFilter{ - TargetNameFilter: &action.TargetNameFilter{ - TargetName: name, - }, - } - response.Details.Timestamp = resp.GetDetails().GetChanged() - - response.Result[0].Details = resp.GetDetails() - response.Result[0].Config.Name = name - return nil - }, - req: &action.SearchTargetsRequest{ - Filters: []*action.TargetSearchFilter{{}}, - }, - }, - want: &action.SearchTargetsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 1, - AppliedLimit: 100, - }, - Result: []*action.GetTarget{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - }, - { - name: "list multiple id", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error { - name1 := gofakeit.Name() - name2 := gofakeit.Name() - name3 := gofakeit.Name() - resp1 := instance.CreateTarget(ctx, t, name1, "https://example.com", domain.TargetTypeWebhook, false) - resp2 := instance.CreateTarget(ctx, t, name2, "https://example.com", domain.TargetTypeCall, true) - resp3 := instance.CreateTarget(ctx, t, name3, "https://example.com", domain.TargetTypeAsync, false) - request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{ - InTargetIdsFilter: &action.InTargetIDsFilter{ - TargetIds: []string{resp1.GetDetails().GetId(), resp2.GetDetails().GetId(), resp3.GetDetails().GetId()}, - }, - } - response.Details.Timestamp = resp3.GetDetails().GetChanged() - - response.Result[0].Details = resp1.GetDetails() - response.Result[0].Config.Name = name1 - response.Result[1].Details = resp2.GetDetails() - response.Result[1].Config.Name = name2 - response.Result[2].Details = resp3.GetDetails() - response.Result[2].Config.Name = name3 - return nil - }, - req: &action.SearchTargetsRequest{ - Filters: []*action.TargetSearchFilter{{}}, - }, - }, - want: &action.SearchTargetsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 3, - AppliedLimit: 100, - }, - Result: []*action.GetTarget{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.dep != nil { - err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) - require.NoError(t, err) - } - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, listErr := instance.Client.ActionV3Alpha.SearchTargets(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(ttt, listErr, "Error: "+listErr.Error()) - return - } - require.NoError(ttt, listErr) - - // always first check length, otherwise its failed anyway - if assert.Len(ttt, got.Result, len(tt.want.Result)) { - for i := range tt.want.Result { - integration.AssertResourceDetails(ttt, tt.want.Result[i].GetDetails(), got.Result[i].GetDetails()) - assert.EqualExportedValues(ttt, tt.want.Result[i].GetConfig(), got.Result[i].GetConfig()) - assert.NotEmpty(ttt, got.Result[i].GetSigningKey()) - } - } - integration.AssertResourceListDetails(ttt, tt.want, got) - }, retryDuration, tick, "timeout waiting for expected execution result") - }) - } -} - -func TestServer_SearchExecutions(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) - - type args struct { - ctx context.Context - dep func(context.Context, *action.SearchExecutionsRequest, *action.SearchExecutionsResponse) error - req *action.SearchExecutionsRequest - } - tests := []struct { - name string - args args - want *action.SearchExecutionsResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SearchExecutionsRequest{}, - }, - wantErr: true, - }, - { - name: "list request single condition", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { - cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0] - resp := instance.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetDetails().GetId())) - - response.Details.Timestamp = resp.GetDetails().GetChanged() - // Set expected response with used values for SetExecution - response.Result[0].Details = resp.GetDetails() - response.Result[0].Condition = cond - return nil - }, - req: &action.SearchExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{{ - Filter: &action.ExecutionSearchFilter_InConditionsFilter{ - InConditionsFilter: &action.InConditionsFilter{ - Conditions: []*action.Condition{{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }}, - }, - }, - }}, - }, - }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 1, - AppliedLimit: 100, - }, - Result: []*action.GetExecution{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - }, - }, - }, - { - name: "list request single target", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { - target := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) - // add target as Filter to the request - request.Filters[0] = &action.ExecutionSearchFilter{ - Filter: &action.ExecutionSearchFilter_TargetFilter{ - TargetFilter: &action.TargetFilter{ - TargetId: target.GetDetails().GetId(), - }, - }, - } - cond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.management.v1.ManagementService/UpdateAction", - }, - }, - }, - } - targets := executionTargetsSingleTarget(target.GetDetails().GetId()) - resp := instance.SetExecution(ctx, t, cond, targets) - - response.Details.Timestamp = resp.GetDetails().GetChanged() - - response.Result[0].Details = resp.GetDetails() - response.Result[0].Condition = cond - response.Result[0].Execution.Targets = targets - return nil - }, - req: &action.SearchExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{{}}, - }, - }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 1, - AppliedLimit: 100, - }, - Result: []*action.GetExecution{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Condition: &action.Condition{}, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(""), - }, - }, - }, - }, - }, { - name: "list request single include", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { - cond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.management.v1.ManagementService/GetAction", - }, - }, - }, - } - instance.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetDetails().GetId())) - request.Filters[0].GetIncludeFilter().Include = cond - - includeCond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.management.v1.ManagementService/ListActions", - }, - }, - }, - } - includeTargets := executionTargetsSingleInclude(cond) - resp2 := instance.SetExecution(ctx, t, includeCond, includeTargets) - - response.Details.Timestamp = resp2.GetDetails().GetChanged() - - response.Result[0].Details = resp2.GetDetails() - response.Result[0].Condition = includeCond - response.Result[0].Execution = &action.Execution{ - Targets: includeTargets, - } - return nil - }, - req: &action.SearchExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{{ - Filter: &action.ExecutionSearchFilter_IncludeFilter{ - IncludeFilter: &action.IncludeFilter{}, - }, - }}, - }, - }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 1, - AppliedLimit: 100, - }, - Result: []*action.GetExecution{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - }, - }, - }, - }, - { - name: "list multiple conditions", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { - - cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0] - targets1 := executionTargetsSingleTarget(targetResp.GetDetails().GetId()) - resp1 := instance.SetExecution(ctx, t, cond1, targets1) - response.Result[0].Details = resp1.GetDetails() - response.Result[0].Condition = cond1 - response.Result[0].Execution = &action.Execution{ - Targets: targets1, - } - - cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1] - targets2 := executionTargetsSingleTarget(targetResp.GetDetails().GetId()) - resp2 := instance.SetExecution(ctx, t, cond2, targets2) - response.Result[1].Details = resp2.GetDetails() - response.Result[1].Condition = cond2 - response.Result[1].Execution = &action.Execution{ - Targets: targets2, - } - - cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2] - targets3 := executionTargetsSingleTarget(targetResp.GetDetails().GetId()) - resp3 := instance.SetExecution(ctx, t, cond3, targets3) - response.Result[2].Details = resp3.GetDetails() - response.Result[2].Condition = cond3 - response.Result[2].Execution = &action.Execution{ - Targets: targets3, - } - response.Details.Timestamp = resp3.GetDetails().GetChanged() - return nil - }, - req: &action.SearchExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{{ - Filter: &action.ExecutionSearchFilter_InConditionsFilter{ - InConditionsFilter: &action.InConditionsFilter{ - Conditions: []*action.Condition{ - { - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }, - { - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/CreateSession", - }, - }, - }, - }, - { - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/SetSession", - }, - }, - }, - }, - }, - }, - }, - }}, - }, - }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 3, - AppliedLimit: 100, - }, - Result: []*action.GetExecution{ - { - Details: &resource_object.Details{ - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}, - }, - }, { - Details: &resource_object.Details{ - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}, - }, - }, { - Details: &resource_object.Details{ - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}, - }, - }, - }, - }, - }, - { - name: "list multiple conditions all types", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { - targets := executionTargetsSingleTarget(targetResp.GetDetails().GetId()) - for i, cond := range request.Filters[0].GetInConditionsFilter().GetConditions() { - resp := instance.SetExecution(ctx, t, cond, targets) - response.Result[i].Details = resp.GetDetails() - response.Result[i].Condition = cond - response.Result[i].Execution = &action.Execution{ - Targets: targets, - } - // filled with info of last sequence - response.Details.Timestamp = resp.GetDetails().GetChanged() - } - - return nil - }, - req: &action.SearchExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{{ - Filter: &action.ExecutionSearchFilter_InConditionsFilter{ - InConditionsFilter: &action.InConditionsFilter{ - Conditions: []*action.Condition{ - {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, - {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, - {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, - {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, - {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, - {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, - {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, - {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, - {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}, - {ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}}}, - }, - }, - }, - }}, - }, - }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 10, - AppliedLimit: 100, - }, - Result: []*action.GetExecution{ - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.dep != nil { - err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) - require.NoError(t, err) - } - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, listErr := instance.Client.ActionV3Alpha.SearchExecutions(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(ttt, listErr, "Error: "+listErr.Error()) - return - } - require.NoError(ttt, listErr) - // always first check length, otherwise its failed anyway - if assert.Len(ttt, got.Result, len(tt.want.Result)) { - for i := range tt.want.Result { - // as not sorted, all elements have to be checked - // workaround as oneof elements can only be checked with assert.EqualExportedValues() - if j, found := containExecution(got.Result, tt.want.Result[i]); found { - integration.AssertResourceDetails(ttt, tt.want.Result[i].GetDetails(), got.Result[j].GetDetails()) - got.Result[j].Details = tt.want.Result[i].GetDetails() - assert.EqualExportedValues(ttt, tt.want.Result[i], got.Result[j]) - } - } - } - integration.AssertResourceListDetails(ttt, tt.want, got) - }, retryDuration, tick, "timeout waiting for expected execution result") - }) - } -} - -func containExecution(executionList []*action.GetExecution, execution *action.GetExecution) (int, bool) { - for i, exec := range executionList { - if reflect.DeepEqual(exec.Details, execution.Details) { - return i, true - } - } - return 0, false -} diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/server_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/server_test.go deleted file mode 100644 index bc8e43eafc..0000000000 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/server_test.go +++ /dev/null @@ -1,69 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "os" - "testing" - "time" - - "github.com/muhlemmer/gu" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/pkg/grpc/feature/v2" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" -) - -var ( - CTX context.Context -) - -func TestMain(m *testing.M) { - os.Exit(func() int { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) - defer cancel() - CTX = ctx - return m.Run() - }()) -} - -func ensureFeatureEnabled(t *testing.T, instance *integration.Instance) { - ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ - Inheritance: true, - }) - require.NoError(t, err) - if f.Actions.GetEnabled() { - return - } - _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ - Actions: gu.Ptr(true), - }) - require.NoError(t, err) - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) - require.EventuallyWithT(t, - func(ttt *assert.CollectT) { - f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ - Inheritance: true, - }) - assert.NoError(ttt, err) - assert.True(ttt, f.Actions.GetEnabled()) - }, - retryDuration, - tick, - "timed out waiting for ensuring instance feature") - - retryDuration, tick = integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) - require.EventuallyWithT(t, - func(ttt *assert.CollectT) { - _, err := instance.Client.ActionV3Alpha.ListExecutionMethods(ctx, &action.ListExecutionMethodsRequest{}) - assert.NoError(ttt, err) - }, - retryDuration, - tick, - "timed out waiting for ensuring instance feature call") -} diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/target_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/target_test.go deleted file mode 100644 index b5d1903ca6..0000000000 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/target_test.go +++ /dev/null @@ -1,499 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "testing" - "time" - - "github.com/brianvoe/gofakeit/v6" - "github.com/muhlemmer/gu" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" -) - -func TestServer_CreateTarget(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - tests := []struct { - name string - ctx context.Context - req *action.Target - want *resource_object.Details - wantErr bool - }{ - { - name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.Target{ - Name: gofakeit.Name(), - }, - wantErr: true, - }, - { - name: "empty name", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: "", - }, - wantErr: true, - }, - { - name: "empty type", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - TargetType: nil, - }, - wantErr: true, - }, - { - name: "empty webhook url", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{}, - }, - }, - wantErr: true, - }, - { - name: "empty request response url", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{}, - }, - }, - wantErr: true, - }, - { - name: "empty timeout", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{}, - }, - Timeout: nil, - }, - wantErr: true, - }, - { - name: "async, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - Endpoint: "https://example.com", - TargetType: &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - { - name: "webhook, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - { - name: "webhook, interrupt on error, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - { - name: "call, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - - { - name: "call, interruptOnError, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := instance.Client.ActionV3Alpha.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req}) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - integration.AssertResourceDetails(t, tt.want, got.Details) - assert.NotEmpty(t, got.GetSigningKey()) - } - }) - } -} - -func TestServer_PatchTarget(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - type args struct { - ctx context.Context - req *action.PatchTargetRequest - } - type want struct { - details *resource_object.Details - signingKey bool - } - tests := []struct { - name string - prepare func(request *action.PatchTargetRequest) error - args args - want want - wantErr bool - }{ - { - name: "missing permission", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - Name: gu.Ptr(gofakeit.Name()), - }, - }, - }, - wantErr: true, - }, - { - name: "not existing", - prepare: func(request *action.PatchTargetRequest) error { - request.Id = "notexisting" - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - Name: gu.Ptr(gofakeit.Name()), - }, - }, - }, - wantErr: true, - }, - { - name: "change name, ok", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - Name: gu.Ptr(gofakeit.Name()), - }, - }, - }, - want: want{ - details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "regenerate signingkey, ok", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - ExpirationSigningKey: durationpb.New(0 * time.Second), - }, - }, - }, - want: want{ - details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - signingKey: true, - }, - }, - { - name: "change type, ok", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - TargetType: &action.PatchTarget_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - }, - }, - }, - want: want{ - details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "change url, ok", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - Endpoint: gu.Ptr("https://example.com/hooks/new"), - }, - }, - }, - want: want{ - details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "change timeout, ok", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - Timeout: durationpb.New(20 * time.Second), - }, - }, - }, - want: want{ - details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "change type async, ok", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - TargetType: &action.PatchTarget_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - }, - }, - }, - want: want{ - details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.prepare(tt.args.req) - require.NoError(t, err) - // We want to have the same response no matter how often we call the function - instance.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req) - got, err := instance.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - integration.AssertResourceDetails(t, tt.want.details, got.Details) - if tt.want.signingKey { - assert.NotEmpty(t, got.SigningKey) - } - } - }) - } -} - -func TestServer_DeleteTarget(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - target := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false) - tests := []struct { - name string - ctx context.Context - req *action.DeleteTargetRequest - want *resource_object.Details - wantErr bool - }{ - { - name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.DeleteTargetRequest{ - Id: target.GetDetails().GetId(), - }, - wantErr: true, - }, - { - name: "empty id", - ctx: iamOwnerCtx, - req: &action.DeleteTargetRequest{ - Id: "", - }, - wantErr: true, - }, - { - name: "delete target", - ctx: iamOwnerCtx, - req: &action.DeleteTargetRequest{ - Id: target.GetDetails().GetId(), - }, - want: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := instance.Client.ActionV3Alpha.DeleteTarget(tt.ctx, tt.req) - if tt.wantErr { - assert.Error(t, err) - return - } else { - assert.NoError(t, err) - integration.AssertResourceDetails(t, tt.want, got.Details) - } - }) - } -} diff --git a/internal/api/grpc/resources/webkey/v3/webkey_converter.go b/internal/api/grpc/resources/webkey/v3/webkey_converter.go deleted file mode 100644 index b460775dd5..0000000000 --- a/internal/api/grpc/resources/webkey/v3/webkey_converter.go +++ /dev/null @@ -1,173 +0,0 @@ -package webkey - -import ( - resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" - "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" -) - -func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig { - switch config := req.GetKey().GetConfig().(type) { - case *webkey.WebKey_Rsa: - return webKeyRSAConfigToCrypto(config.Rsa) - case *webkey.WebKey_Ecdsa: - return webKeyECDSAConfigToCrypto(config.Ecdsa) - case *webkey.WebKey_Ed25519: - return new(crypto.WebKeyED25519Config) - default: - return webKeyRSAConfigToCrypto(nil) - } -} - -func webKeyRSAConfigToCrypto(config *webkey.WebKeyRSAConfig) *crypto.WebKeyRSAConfig { - out := new(crypto.WebKeyRSAConfig) - - switch config.GetBits() { - case webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED: - out.Bits = crypto.RSABits2048 - case webkey.WebKeyRSAConfig_RSA_BITS_2048: - out.Bits = crypto.RSABits2048 - case webkey.WebKeyRSAConfig_RSA_BITS_3072: - out.Bits = crypto.RSABits3072 - case webkey.WebKeyRSAConfig_RSA_BITS_4096: - out.Bits = crypto.RSABits4096 - default: - out.Bits = crypto.RSABits2048 - } - - switch config.GetHasher() { - case webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED: - out.Hasher = crypto.RSAHasherSHA256 - case webkey.WebKeyRSAConfig_RSA_HASHER_SHA256: - out.Hasher = crypto.RSAHasherSHA256 - case webkey.WebKeyRSAConfig_RSA_HASHER_SHA384: - out.Hasher = crypto.RSAHasherSHA384 - case webkey.WebKeyRSAConfig_RSA_HASHER_SHA512: - out.Hasher = crypto.RSAHasherSHA512 - default: - out.Hasher = crypto.RSAHasherSHA256 - } - - return out -} - -func webKeyECDSAConfigToCrypto(config *webkey.WebKeyECDSAConfig) *crypto.WebKeyECDSAConfig { - out := new(crypto.WebKeyECDSAConfig) - - switch config.GetCurve() { - case webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED: - out.Curve = crypto.EllipticCurveP256 - case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256: - out.Curve = crypto.EllipticCurveP256 - case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384: - out.Curve = crypto.EllipticCurveP384 - case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512: - out.Curve = crypto.EllipticCurveP512 - default: - out.Curve = crypto.EllipticCurveP256 - } - - return out -} - -func webKeyDetailsListToPb(list []query.WebKeyDetails, instanceID string) []*webkey.GetWebKey { - out := make([]*webkey.GetWebKey, len(list)) - for i := range list { - out[i] = webKeyDetailsToPb(&list[i], instanceID) - } - return out -} - -func webKeyDetailsToPb(details *query.WebKeyDetails, instanceID string) *webkey.GetWebKey { - out := &webkey.GetWebKey{ - Details: resource_object.DomainToDetailsPb(&domain.ObjectDetails{ - ID: details.KeyID, - CreationDate: details.CreationDate, - EventDate: details.ChangeDate, - }, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), - State: webKeyStateToPb(details.State), - Config: &webkey.WebKey{}, - } - - switch config := details.Config.(type) { - case *crypto.WebKeyRSAConfig: - out.Config.Config = &webkey.WebKey_Rsa{ - Rsa: webKeyRSAConfigToPb(config), - } - case *crypto.WebKeyECDSAConfig: - out.Config.Config = &webkey.WebKey_Ecdsa{ - Ecdsa: webKeyECDSAConfigToPb(config), - } - case *crypto.WebKeyED25519Config: - out.Config.Config = &webkey.WebKey_Ed25519{ - Ed25519: new(webkey.WebKeyED25519Config), - } - } - - return out -} - -func webKeyStateToPb(state domain.WebKeyState) webkey.WebKeyState { - switch state { - case domain.WebKeyStateUnspecified: - return webkey.WebKeyState_STATE_UNSPECIFIED - case domain.WebKeyStateInitial: - return webkey.WebKeyState_STATE_INITIAL - case domain.WebKeyStateActive: - return webkey.WebKeyState_STATE_ACTIVE - case domain.WebKeyStateInactive: - return webkey.WebKeyState_STATE_INACTIVE - case domain.WebKeyStateRemoved: - return webkey.WebKeyState_STATE_REMOVED - default: - return webkey.WebKeyState_STATE_UNSPECIFIED - } -} - -func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.WebKeyRSAConfig { - out := new(webkey.WebKeyRSAConfig) - - switch config.Bits { - case crypto.RSABitsUnspecified: - out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED - case crypto.RSABits2048: - out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_2048 - case crypto.RSABits3072: - out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_3072 - case crypto.RSABits4096: - out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_4096 - } - - switch config.Hasher { - case crypto.RSAHasherUnspecified: - out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED - case crypto.RSAHasherSHA256: - out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA256 - case crypto.RSAHasherSHA384: - out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA384 - case crypto.RSAHasherSHA512: - out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA512 - } - - return out -} - -func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.WebKeyECDSAConfig { - out := new(webkey.WebKeyECDSAConfig) - - switch config.Curve { - case crypto.EllipticCurveUnspecified: - out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED - case crypto.EllipticCurveP256: - out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256 - case crypto.EllipticCurveP384: - out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384 - case crypto.EllipticCurveP512: - out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512 - } - - return out -} diff --git a/internal/api/grpc/saml/v2/saml.go b/internal/api/grpc/saml/v2/saml.go index 866846dfd7..43eae5feb1 100644 --- a/internal/api/grpc/saml/v2/saml.go +++ b/internal/api/grpc/saml/v2/saml.go @@ -4,9 +4,11 @@ import ( "context" "github.com/zitadel/logging" + "github.com/zitadel/saml/pkg/provider" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/saml" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -76,6 +78,11 @@ func (s *Server) linkSessionToSAMLRequest(ctx context.Context, samlRequestID str return nil, err } authReq := &saml.AuthRequestV2{CurrentSAMLRequest: aar} + responseIssuer := authReq.ResponseIssuer + if responseIssuer == "" { + responseIssuer = http_utils.DomainContext(ctx).Origin() + } + ctx = provider.ContextWithIssuer(ctx, responseIssuer) url, body, err := s.idp.CreateResponse(ctx, authReq) if err != nil { return nil, err diff --git a/internal/api/grpc/server/middleware/access_interceptor.go b/internal/api/grpc/server/middleware/access_interceptor.go index 100264c3f5..f95c3225ed 100644 --- a/internal/api/grpc/server/middleware/access_interceptor.go +++ b/internal/api/grpc/server/middleware/access_interceptor.go @@ -20,7 +20,6 @@ func AccessStorageInterceptor(svc *logstore.Service[*record.AccessLog]) grpc.Una if !svc.Enabled() { return handler(ctx, req) } - reqMd, _ := metadata.FromIncomingContext(ctx) resp, handlerErr := handler(ctx, req) diff --git a/internal/api/grpc/server/middleware/auth_interceptor.go b/internal/api/grpc/server/middleware/auth_interceptor.go index 6eb326a59a..410b4b8abc 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor.go +++ b/internal/api/grpc/server/middleware/auth_interceptor.go @@ -13,13 +13,13 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor { +func AuthorizationInterceptor(verifier authz.APITokenVerifier, systemUserPermissions authz.Config, authConfig authz.Config) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - return authorize(ctx, req, info, handler, verifier, authConfig) + return authorize(ctx, req, info, handler, verifier, systemUserPermissions, authConfig) } } -func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, authConfig authz.Config) (_ interface{}, err error) { +func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, systemUserPermissions authz.Config, authConfig authz.Config) (_ interface{}, err error) { authOpt, needsToken := verifier.CheckAuthMethod(info.FullMethod) if !needsToken { return handler(ctx, req) @@ -34,7 +34,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, } orgID, orgDomain := orgIDAndDomainFromRequest(authCtx, req) - ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, authConfig, authOpt, info.FullMethod) + ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, systemUserPermissions.RolePermissionMappings, authConfig.RolePermissionMappings, authOpt, info.FullMethod) if err != nil { return nil, err } diff --git a/internal/api/grpc/server/middleware/auth_interceptor_test.go b/internal/api/grpc/server/middleware/auth_interceptor_test.go index 3551d3e419..e098189445 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor_test.go +++ b/internal/api/grpc/server/middleware/auth_interceptor_test.go @@ -20,6 +20,7 @@ type authzRepoMock struct{} func (v *authzRepoMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) { return "", "", "", "", "", nil } + func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) { return authz.Memberships{{ MemberType: authz.MemberTypeOrganization, @@ -31,9 +32,11 @@ func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ func (v *authzRepoMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) { return "", nil, nil } + func (v *authzRepoMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) { return orgID, nil } + func (v *authzRepoMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) { return "", "", nil } @@ -252,7 +255,7 @@ func Test_authorize(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := authorize(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier(), tt.args.authConfig) + got, err := authorize(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier(), tt.args.authConfig, tt.args.authConfig) if (err != nil) != tt.res.wantErr { t.Errorf("authorize() error = %v, wantErr %v", err, tt.res.wantErr) return diff --git a/internal/api/grpc/server/middleware/execution_interceptor.go b/internal/api/grpc/server/middleware/execution_interceptor.go index c309827d94..4aeea6c4da 100644 --- a/internal/api/grpc/server/middleware/execution_interceptor.go +++ b/internal/api/grpc/server/middleware/execution_interceptor.go @@ -3,23 +3,20 @@ package middleware import ( "context" "encoding/json" - "strings" - "github.com/zitadel/logging" "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/query" - exec_repo "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/zerrors" ) func ExecutionHandler(queries *query.Queries) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - requestTargets, responseTargets := queryTargets(ctx, queries, info.FullMethod) + requestTargets, responseTargets := execution.QueryExecutionTargetsForRequestAndResponse(ctx, queries, info.FullMethod) // call targets otherwise return req handledReq, err := executeTargetsForRequest(ctx, requestTargets, info.FullMethod, req) @@ -38,7 +35,7 @@ func ExecutionHandler(queries *query.Queries) grpc.UnaryServerInterceptor { func executeTargetsForRequest(ctx context.Context, targets []execution.Target, fullMethod string, req interface{}) (_ interface{}, err error) { ctx, span := tracing.NewSpan(ctx) - defer span.EndWithError(err) + defer func() { span.EndWithError(err) }() // if no targets are found, return without any calls if len(targets) == 0 { @@ -52,7 +49,7 @@ func executeTargetsForRequest(ctx context.Context, targets []execution.Target, f ProjectID: ctxData.ProjectID, OrgID: ctxData.OrgID, UserID: ctxData.UserID, - Request: req, + Request: Message{req.(proto.Message)}, } return execution.CallTargets(ctx, targets, info) @@ -60,7 +57,7 @@ func executeTargetsForRequest(ctx context.Context, targets []execution.Target, f func executeTargetsForResponse(ctx context.Context, targets []execution.Target, fullMethod string, req, resp interface{}) (_ interface{}, err error) { ctx, span := tracing.NewSpan(ctx) - defer span.EndWithError(err) + defer func() { span.EndWithError(err) }() // if no targets are found, return without any calls if len(targets) == 0 { @@ -74,65 +71,38 @@ func executeTargetsForResponse(ctx context.Context, targets []execution.Target, ProjectID: ctxData.ProjectID, OrgID: ctxData.OrgID, UserID: ctxData.UserID, - Request: req, - Response: resp, + Request: Message{req.(proto.Message)}, + Response: Message{resp.(proto.Message)}, } return execution.CallTargets(ctx, targets, info) } -type ExecutionQueries interface { - TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string) (execution []*query.ExecutionTarget, err error) -} - -func queryTargets( - ctx context.Context, - queries ExecutionQueries, - fullMethod string, -) ([]execution.Target, []execution.Target) { - ctx, span := tracing.NewSpan(ctx) - defer span.End() - - targets, err := queries.TargetsByExecutionIDs(ctx, - idsForFullMethod(fullMethod, domain.ExecutionTypeRequest), - idsForFullMethod(fullMethod, domain.ExecutionTypeResponse), - ) - requestTargets := make([]execution.Target, 0, len(targets)) - responseTargets := make([]execution.Target, 0, len(targets)) - if err != nil { - logging.WithFields("fullMethod", fullMethod).WithError(err).Info("unable to query targets") - return requestTargets, responseTargets - } - - for _, target := range targets { - if strings.HasPrefix(target.GetExecutionID(), exec_repo.IDAll(domain.ExecutionTypeRequest)) { - requestTargets = append(requestTargets, target) - } else if strings.HasPrefix(target.GetExecutionID(), exec_repo.IDAll(domain.ExecutionTypeResponse)) { - responseTargets = append(responseTargets, target) - } - } - - return requestTargets, responseTargets -} - -func idsForFullMethod(fullMethod string, executionType domain.ExecutionType) []string { - return []string{exec_repo.ID(executionType, fullMethod), exec_repo.ID(executionType, serviceFromFullMethod(fullMethod)), exec_repo.IDAll(executionType)} -} - -func serviceFromFullMethod(s string) string { - parts := strings.Split(s, "/") - return parts[1] -} - var _ execution.ContextInfo = &ContextInfoRequest{} type ContextInfoRequest struct { - FullMethod string `json:"fullMethod,omitempty"` - InstanceID string `json:"instanceID,omitempty"` - OrgID string `json:"orgID,omitempty"` - ProjectID string `json:"projectID,omitempty"` - UserID string `json:"userID,omitempty"` - Request interface{} `json:"request,omitempty"` + FullMethod string `json:"fullMethod,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + OrgID string `json:"orgID,omitempty"` + ProjectID string `json:"projectID,omitempty"` + UserID string `json:"userID,omitempty"` + Request Message `json:"request,omitempty"` +} + +type Message struct { + proto.Message +} + +func (r *Message) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r.Message) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *Message) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r.Message) } func (c *ContextInfoRequest) GetHTTPRequestBody() []byte { @@ -144,26 +114,23 @@ func (c *ContextInfoRequest) GetHTTPRequestBody() []byte { } func (c *ContextInfoRequest) SetHTTPResponseBody(resp []byte) error { - if !json.Valid(resp) { - return zerrors.ThrowPreconditionFailed(nil, "ACTION-4m9s2", "Errors.Execution.ResponseIsNotValidJSON") - } - return json.Unmarshal(resp, c.Request) + return json.Unmarshal(resp, &c.Request) } func (c *ContextInfoRequest) GetContent() interface{} { - return c.Request + return c.Request.Message } var _ execution.ContextInfo = &ContextInfoResponse{} type ContextInfoResponse struct { - FullMethod string `json:"fullMethod,omitempty"` - InstanceID string `json:"instanceID,omitempty"` - OrgID string `json:"orgID,omitempty"` - ProjectID string `json:"projectID,omitempty"` - UserID string `json:"userID,omitempty"` - Request interface{} `json:"request,omitempty"` - Response interface{} `json:"response,omitempty"` + FullMethod string `json:"fullMethod,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + OrgID string `json:"orgID,omitempty"` + ProjectID string `json:"projectID,omitempty"` + UserID string `json:"userID,omitempty"` + Request Message `json:"request,omitempty"` + Response Message `json:"response,omitempty"` } func (c *ContextInfoResponse) GetHTTPRequestBody() []byte { @@ -175,9 +142,9 @@ func (c *ContextInfoResponse) GetHTTPRequestBody() []byte { } func (c *ContextInfoResponse) SetHTTPResponseBody(resp []byte) error { - return json.Unmarshal(resp, c.Response) + return json.Unmarshal(resp, &c.Response) } func (c *ContextInfoResponse) GetContent() interface{} { - return c.Response + return c.Response.Message } diff --git a/internal/api/grpc/server/middleware/execution_interceptor_test.go b/internal/api/grpc/server/middleware/execution_interceptor_test.go index 6a5b74c5e4..281db4617a 100644 --- a/internal/api/grpc/server/middleware/execution_interceptor_test.go +++ b/internal/api/grpc/server/middleware/execution_interceptor_test.go @@ -11,6 +11,9 @@ import ( "time" "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/execution" @@ -54,28 +57,28 @@ func (e *mockExecutionTarget) GetSigningKey() string { return e.SigningKey } -type mockContentRequest struct { - Content string -} - -func newMockContentRequest(content string) *mockContentRequest { - return &mockContentRequest{ - Content: content, +func newMockContentRequest(content string) proto.Message { + return &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "content": { + Kind: &structpb.Value_StringValue{StringValue: content}, + }, + }, } } func newMockContextInfoRequest(fullMethod, request string) *ContextInfoRequest { return &ContextInfoRequest{ FullMethod: fullMethod, - Request: newMockContentRequest(request), + Request: Message{Message: newMockContentRequest(request)}, } } func newMockContextInfoResponse(fullMethod, request, response string) *ContextInfoResponse { return &ContextInfoResponse{ FullMethod: fullMethod, - Request: newMockContentRequest(request), - Response: newMockContentRequest(response), + Request: Message{Message: newMockContentRequest(request)}, + Response: Message{Message: newMockContentRequest(response)}, } } @@ -591,7 +594,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { } else { assert.NoError(t, err) } - assert.Equal(t, tt.res.want, resp) + assert.EqualExportedValues(t, tt.res.want, resp) for _, closeF := range closeFuncs { closeF() @@ -632,7 +635,7 @@ func testServerCall( time.Sleep(sleep) w.Header().Set("Content-Type", "application/json") - resp, err := json.Marshal(respBody) + resp, err := protojson.Marshal(respBody.(proto.Message)) if err != nil { http.Error(w, "error", http.StatusInternalServerError) return @@ -723,7 +726,8 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) { statusCode: http.StatusOK, }, }, - req: []byte{}, + req: newMockContentRequest(""), + resp: newMockContentRequest(""), }, res{ wantErr: true, @@ -790,7 +794,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) { } else { assert.NoError(t, err) } - assert.Equal(t, tt.res.want, resp) + assert.EqualExportedValues(t, tt.res.want, resp) for _, closeF := range closeFuncs { closeF() diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 27b921b7d5..b686d3add9 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -36,6 +36,7 @@ type WithGatewayPrefix interface { func CreateServer( verifier authz.APITokenVerifier, + systemAuthz authz.Config, authConfig authz.Config, queries *query.Queries, externalDomain string, @@ -53,7 +54,7 @@ func CreateServer( middleware.AccessStorageInterceptor(accessSvc), middleware.ErrorHandler(), middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName), - middleware.AuthorizationInterceptor(verifier, authConfig), + middleware.AuthorizationInterceptor(verifier, systemAuthz, authConfig), middleware.TranslationHandler(), middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName), middleware.ExecutionHandler(queries), diff --git a/internal/api/grpc/session/v2/session.go b/internal/api/grpc/session/v2/session.go index 7562d64350..08f19368ef 100644 --- a/internal/api/grpc/session/v2/session.go +++ b/internal/api/grpc/session/v2/session.go @@ -255,7 +255,7 @@ type userSearchByID struct { } func (u userSearchByID) search(ctx context.Context, q *query.Queries) (*query.User, error) { - return q.GetUserByID(ctx, true, u.id) + return q.GetUserByID(ctx, false, u.id) } type userSearchByLoginName struct { diff --git a/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go b/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go index 24c224b0fe..b705618f68 100644 --- a/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go +++ b/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go @@ -73,6 +73,7 @@ func requireEventually( assertCounts func(assert.TestingT, *eventCounts), msg string, ) (counts *eventCounts) { + t.Helper() countTimeout := 30 * time.Second assertTimeout := countTimeout + time.Second countCtx, cancel := context.WithTimeout(ctx, time.Minute) diff --git a/internal/api/grpc/user/v2/integration_test/query_test.go b/internal/api/grpc/user/v2/integration_test/query_test.go index 554de2b69a..15dc959151 100644 --- a/internal/api/grpc/user/v2/integration_test/query_test.go +++ b/internal/api/grpc/user/v2/integration_test/query_test.go @@ -415,6 +415,10 @@ func createUsers(ctx context.Context, orgID string, count int, passwordChangeReq func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) userAttr { username := gofakeit.Email() + return createUserWithUserName(ctx, username, orgID, passwordChangeRequired) +} + +func createUserWithUserName(ctx context.Context, username string, orgID string, passwordChangeRequired bool) userAttr { // used as default country prefix phone := "+41" + gofakeit.Phone() resp := Instance.CreateHumanUserVerified(ctx, orgID, username, phone) @@ -1179,6 +1183,97 @@ func TestServer_ListUsers(t *testing.T) { } } +func TestServer_SystemUsers_ListUsers(t *testing.T) { + defer func() { + _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{}) + require.NoError(t, err) + }() + + org1 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email()) + org2 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), "org2@zitadel.com") + org3 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email()) + _ = createUserWithUserName(IamCTX, "Test_SystemUsers_ListUser1@zitadel.com", org1.OrganizationId, false) + _ = createUserWithUserName(IamCTX, "Test_SystemUsers_ListUser2@zitadel.com", org2.OrganizationId, false) + _ = createUserWithUserName(IamCTX, "Test_SystemUsers_ListUser3@zitadel.com", org3.OrganizationId, false) + + tests := []struct { + name string + ctx context.Context + req *user.ListUsersRequest + expectedFoundUsernames []string + checkNumberOfUsersReturned bool + }{ + { + name: "list users with neccessary permissions", + ctx: SystemCTX, + req: &user.ListUsersRequest{}, + // the number of users returned will vary from test run to test run, + // so just check the system user gets back users from different orgs whcih it is not a memeber of + checkNumberOfUsersReturned: false, + expectedFoundUsernames: []string{"Test_SystemUsers_ListUser1@zitadel.com", "Test_SystemUsers_ListUser2@zitadel.com", "Test_SystemUsers_ListUser3@zitadel.com"}, + }, + { + name: "list users without neccessary permissions", + ctx: SystemUserWithNoPermissionsCTX, + req: &user.ListUsersRequest{}, + // check no users returned + checkNumberOfUsersReturned: true, + }, + { + name: "list users with neccessary permissions specifying org", + req: &user.ListUsersRequest{ + Queries: []*user.SearchQuery{OrganizationIdQuery(org2.OrganizationId)}, + }, + ctx: SystemCTX, + expectedFoundUsernames: []string{"Test_SystemUsers_ListUser2@zitadel.com", "org2@zitadel.com"}, + checkNumberOfUsersReturned: true, + }, + { + name: "list users without neccessary permissions specifying org", + req: &user.ListUsersRequest{ + Queries: []*user.SearchQuery{OrganizationIdQuery(org2.OrganizationId)}, + }, + ctx: SystemUserWithNoPermissionsCTX, + // check no users returned + checkNumberOfUsersReturned: true, + }, + } + + for _, f := range permissionCheckV2Settings { + f := f + for _, tt := range tests { + t.Run(f.TestNamePrependString+tt.name, func(t *testing.T) { + setPermissionCheckV2Flag(t, f.SetFlag) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, 1*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListUsers(tt.ctx, tt.req) + require.NoError(ttt, err) + + if tt.checkNumberOfUsersReturned { + require.Equal(t, len(tt.expectedFoundUsernames), len(got.Result)) + } + + if tt.expectedFoundUsernames != nil { + for _, user := range got.Result { + for i, username := range tt.expectedFoundUsernames { + if username == user.Username { + tt.expectedFoundUsernames = tt.expectedFoundUsernames[i+1:] + break + } + } + if len(tt.expectedFoundUsernames) == 0 { + return + } + } + require.FailNow(t, "unable to find all users with specified usernames") + } + }, retryDuration, tick, "timeout waiting for expected user result") + }) + } + } +} + func InUserIDsQuery(ids []string) *user.SearchQuery { return &user.SearchQuery{ Query: &user.SearchQuery_InUserIdsQuery{ 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 0293fd925d..bf396fd25d 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -20,7 +20,6 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration/sink" "github.com/zitadel/zitadel/pkg/grpc/auth" @@ -32,12 +31,13 @@ import ( ) var ( - CTX context.Context - IamCTX context.Context - UserCTX context.Context - SystemCTX context.Context - Instance *integration.Instance - Client user.UserServiceClient + CTX context.Context + IamCTX context.Context + UserCTX context.Context + SystemCTX context.Context + SystemUserWithNoPermissionsCTX context.Context + Instance *integration.Instance + Client user.UserServiceClient ) func TestMain(m *testing.M) { @@ -47,6 +47,7 @@ func TestMain(m *testing.M) { Instance = integration.NewInstance(ctx) + SystemUserWithNoPermissionsCTX = integration.WithSystemUserWithNoPermissionsAuthorization(ctx) UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission) IamCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) SystemCTX = integration.WithSystemAuthorization(ctx) @@ -1307,7 +1308,6 @@ func TestServer_UpdateHumanUser_Permission(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Client.UpdateHumanUser(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) @@ -2114,18 +2114,29 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { } func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId() - intentID := Instance.CreateIntent(CTX, idpID).GetIdpIntent().GetIdpIntentId() + oauthIdpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId() + oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, gofakeit.AppName()).GetId() + samlIdpID := Instance.AddSAMLPostProvider(IamCTX) + ldapIdpID := Instance.AddLDAPProvider(IamCTX) + authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) + require.NoError(t, err) + intentID := authURL.Query().Get("state") - successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "") + successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "") require.NoError(t, err) - successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "user") + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user") require.NoError(t, err) - ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "") + oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "") require.NoError(t, err) - ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "user") + oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user") require.NoError(t, err) - samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), idpID, "id", "") + ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "") + require.NoError(t, err) + ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user") + require.NoError(t, err) + samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "") + require.NoError(t, err) + samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user") require.NoError(t, err) type args struct { ctx context.Context @@ -2160,7 +2171,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { wantErr: true, }, { - name: "retrieve successful intent", + name: "retrieve successful oauth intent", args: args{ CTX, &user.RetrieveIdentityProviderIntentRequest{ @@ -2181,18 +2192,31 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdToken: gu.Ptr("idToken"), }, }, - IdpId: idpID, + IdpId: oauthIdpID, UserId: "id", - UserName: "username", + UserName: "", RawInformation: func() *structpb.Struct { s, err := structpb.NewStruct(map[string]interface{}{ - "sub": "id", - "preferred_username": "username", + "RawInfo": map[string]interface{}{ + "id": "id", + "preferred_username": "username", + }, }) require.NoError(t, err) return s }(), }, + AddHumanUser: &user.AddHumanUserRequest{ + Profile: &user.SetHumanProfile{ + PreferredLanguage: gu.Ptr("und"), + }, + IdpLinks: []*user.IDPLink{ + {IdpId: oauthIdpID, UserId: "id"}, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}}, + }, + }, }, wantErr: false, }, @@ -2219,7 +2243,97 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdToken: gu.Ptr("idToken"), }, }, - IdpId: idpID, + IdpId: oauthIdpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "RawInfo": map[string]interface{}{ + "id": "id", + "preferred_username": "username", + }, + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful oidc intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: oidcSuccessful, + IdpIntentToken: oidcToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(oidcChangeDate), + ResourceOwner: Instance.ID(), + Sequence: oidcSequence, + }, + UserId: "", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: oidcIdpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + "preferred_username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + AddHumanUser: &user.AddHumanUserRequest{ + Username: gu.Ptr("username"), + Profile: &user.SetHumanProfile{ + PreferredLanguage: gu.Ptr("und"), + }, + IdpLinks: []*user.IDPLink{ + {IdpId: oidcIdpID, UserId: "id", UserName: "username"}, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}}, + }, + }, + }, + wantErr: false, + }, + { + name: "retrieve successful oidc intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: oidcSuccessfulWithUserID, + IdpIntentToken: oidcWithUserIDToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(oidcWithUserIDChangeDate), + ResourceOwner: Instance.ID(), + Sequence: oidcWithUserIDSequence, + }, + UserId: "user", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: oidcIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2263,7 +2377,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }(), }, }, - IdpId: idpID, + IdpId: ldapIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2276,6 +2390,18 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { return s }(), }, + AddHumanUser: &user.AddHumanUserRequest{ + Username: gu.Ptr("username"), + Profile: &user.SetHumanProfile{ + PreferredLanguage: gu.Ptr("en"), + }, + IdpLinks: []*user.IDPLink{ + {IdpId: ldapIdpID, UserId: "id", UserName: "username"}, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}}, + }, + }, }, wantErr: false, }, @@ -2309,7 +2435,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }(), }, }, - IdpId: idpID, + IdpId: ldapIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2346,7 +2472,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { Assertion: []byte(""), }, }, - IdpId: idpID, + IdpId: samlIdpID, UserId: "id", UserName: "", RawInformation: func() *structpb.Struct { @@ -2360,6 +2486,56 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { return s }(), }, + AddHumanUser: &user.AddHumanUserRequest{ + Profile: &user.SetHumanProfile{ + PreferredLanguage: gu.Ptr("und"), + }, + IdpLinks: []*user.IDPLink{ + {IdpId: samlIdpID, UserId: "id"}, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}}, + }, + }, + }, + wantErr: false, + }, + { + name: "retrieve successful saml intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: samlSuccessfulWithUserID, + IdpIntentToken: samlWithUserToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(samlWithUserChangeDate), + ResourceOwner: Instance.ID(), + Sequence: samlWithUserSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Saml{ + Saml: &user.IDPSAMLAccessInformation{ + Assertion: []byte(""), + }, + }, + IdpId: samlIdpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": "id", + "attributes": map[string]interface{}{ + "attribute1": []interface{}{"value1"}, + }, + }) + require.NoError(t, err) + return s + }(), + }, + UserId: "user", }, wantErr: false, }, @@ -2369,11 +2545,11 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { got, err := Client.RetrieveIdentityProviderIntent(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) - } else { - require.NoError(t, err) + return } + require.NoError(t, err) - grpc.AllFieldsEqual(t, tt.want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) + assert.EqualExportedValues(t, tt.want, got) }) } } @@ -2873,7 +3049,6 @@ func TestServer_ListAuthenticationFactors(t *testing.T) { assert.ElementsMatch(t, tt.want.GetResult(), got.GetResult()) }, retryDuration, tick, "timeout waiting for expected auth methods result") - }) } } diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go new file mode 100644 index 0000000000..06966edb35 --- /dev/null +++ b/internal/api/grpc/user/v2/intent.go @@ -0,0 +1,370 @@ +package user + +import ( + "context" + "encoding/json" + "errors" + + oidc_pkg "github.com/zitadel/oidc/v3/pkg/oidc" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/idp" + "github.com/zitadel/zitadel/internal/idp/providers/apple" + "github.com/zitadel/zitadel/internal/idp/providers/azuread" + "github.com/zitadel/zitadel/internal/idp/providers/github" + "github.com/zitadel/zitadel/internal/idp/providers/gitlab" + "github.com/zitadel/zitadel/internal/idp/providers/google" + "github.com/zitadel/zitadel/internal/idp/providers/jwt" + "github.com/zitadel/zitadel/internal/idp/providers/ldap" + "github.com/zitadel/zitadel/internal/idp/providers/oauth" + "github.com/zitadel/zitadel/internal/idp/providers/oidc" + "github.com/zitadel/zitadel/internal/idp/providers/saml" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) { + switch t := req.GetContent().(type) { + case *user.StartIdentityProviderIntentRequest_Urls: + return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls) + case *user.StartIdentityProviderIntentRequest_Ldap: + return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap) + default: + return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderIntent not implemented", t) + } +} + +func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) { + state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID)) + if err != nil { + return nil, err + } + _, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), session.PersistentParameters()) + if err != nil { + return nil, err + } + content, redirect := session.GetAuth(ctx) + if redirect { + return &user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content}, + }, nil + } + return &user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_PostForm{ + PostForm: []byte(content), + }, + }, nil +} + +func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) { + intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil) + if err != nil { + return nil, err + } + externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) + if err != nil { + if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil { + return nil, err + } + return nil, err + } + token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes) + if err != nil { + return nil, err + } + return &user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{ + IdpIntent: &user.IDPIntent{ + IdpIntentId: intentWriteModel.AggregateID, + IdpIntentToken: token, + UserId: userID, + }, + }, + }, nil +} + +func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) { + idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID) + if err != nil { + return "", err + } + externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID) + if err != nil { + return "", err + } + queries := []query.SearchQuery{ + idQuery, externalIDQuery, + } + links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, nil) + if err != nil { + return "", err + } + if len(links.Links) == 1 { + return links.Links[0].UserID, nil + } + return "", nil +} + +func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { + provider, err := s.command.GetProvider(ctx, idpID, "", "") + if err != nil { + return nil, "", nil, err + } + ldapProvider, ok := provider.(*ldap.Provider) + if !ok { + return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "IDP-9a02j2n2bh", "Errors.ExternalIDP.IDPTypeNotImplemented") + } + session := ldapProvider.GetSession(username, password) + externalUser, err := session.FetchUser(ctx) + if errors.Is(err, ldap.ErrFailedLogin) || errors.Is(err, ldap.ErrNoSingleUser) { + return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nzun2i", "Errors.User.ExternalIDP.LoginFailed") + } + if err != nil { + return nil, "", nil, err + } + userID, err := s.checkLinkedExternalUser(ctx, idpID, externalUser.GetID()) + if err != nil { + return nil, "", nil, err + } + + attributes := make(map[string][]string, 0) + for _, item := range session.Entry.Attributes { + attributes[item.Name] = item.Values + } + return externalUser, userID, attributes, nil +} + +func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { + intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), "") + if err != nil { + return nil, err + } + if err := s.checkIntentToken(req.GetIdpIntentToken(), intent.AggregateID); err != nil { + return nil, err + } + if intent.State != domain.IDPIntentStateSucceeded { + return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") + } + idpIntent, err := idpIntentToIDPIntentPb(intent, s.idpAlg) + if err != nil { + return nil, err + } + if idpIntent.UserId == "" { + provider, err := s.command.GetProvider(ctx, idpIntent.IdpInformation.IdpId, "", "") + if err != nil && !errors.Is(err, oidc_pkg.ErrDiscoveryFailed) { + return nil, err + } + var idpUser idp.User + switch p := provider.(type) { + case *apple.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &apple.User{}) + case *oauth.Provider: + idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User()) + case *oidc.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}) + case *jwt.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &jwt.User{}) + case *azuread.Provider: + idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User()) + case *github.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &github.User{}) + case *gitlab.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}) + case *google.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &google.User{User: &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}}) + case *saml.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &saml.UserMapper{}) + case *ldap.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &ldap.User{}) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "IDP-7rPBbls4Zn", "Errors.ExternalIDP.IDPTypeNotImplemented") + } + if err != nil { + return nil, err + } + idpIntent.AddHumanUser = idpUserToAddHumanUser(idpUser, idpIntent.IdpInformation.IdpId) + } + return idpIntent, nil +} + +type rawUserMapper struct { + RawInfo map[string]interface{} +} + +func unmarshalRawIdpUser(idpUserData []byte, idpUser idp.User) (idp.User, error) { + userMapper := &rawUserMapper{} + if err := json.Unmarshal(idpUserData, userMapper); err != nil { + return nil, err + } + idpUserData, err := json.Marshal(userMapper.RawInfo) + if err != nil { + return nil, err + } + return unmarshalIdpUser(idpUserData, idpUser) +} + +func unmarshalIdpUser(idpUserData []byte, idpUser idp.User) (idp.User, error) { + if err := json.Unmarshal(idpUserData, idpUser); err != nil { + return nil, err + } + return idpUser, nil +} + +func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { + rawInformation := new(structpb.Struct) + err = rawInformation.UnmarshalJSON(intent.IDPUser) + if err != nil { + return nil, err + } + information := &user.RetrieveIdentityProviderIntentResponse{ + IdpInformation: &user.IDPInformation{ + IdpId: intent.IDPID, + UserId: intent.IDPUserID, + UserName: intent.IDPUserName, + RawInformation: rawInformation, + }, + UserId: intent.UserID, + } + information.Details = intentToDetailsPb(intent) + // OAuth / OIDC + if intent.IDPIDToken != "" || intent.IDPAccessToken != nil { + information.IdpInformation.Access, err = idpOAuthTokensToPb(intent.IDPIDToken, intent.IDPAccessToken, alg) + if err != nil { + return nil, err + } + } + // LDAP + if intent.IDPEntryAttributes != nil { + access, err := IDPEntryAttributesToPb(intent.IDPEntryAttributes) + if err != nil { + return nil, err + } + information.IdpInformation.Access = access + } + // SAML + if intent.Assertion != nil { + assertion, err := crypto.Decrypt(intent.Assertion, alg) + if err != nil { + return nil, err + } + information.IdpInformation.Access = IDPSAMLResponseToPb(assertion) + } + return information, nil +} + +func idpOAuthTokensToPb(idpIDToken string, idpAccessToken *crypto.CryptoValue, alg crypto.EncryptionAlgorithm) (_ *user.IDPInformation_Oauth, err error) { + var idToken *string + if idpIDToken != "" { + idToken = &idpIDToken + } + var accessToken string + if idpAccessToken != nil { + accessToken, err = crypto.DecryptString(idpAccessToken, alg) + if err != nil { + return nil, err + } + } + return &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: accessToken, + IdToken: idToken, + }, + }, nil +} + +func intentToDetailsPb(intent *command.IDPIntentWriteModel) *object_pb.Details { + return &object_pb.Details{ + Sequence: intent.ProcessedSequence, + ChangeDate: timestamppb.New(intent.ChangeDate), + ResourceOwner: intent.ResourceOwner, + } +} + +func IDPEntryAttributesToPb(entryAttributes map[string][]string) (*user.IDPInformation_Ldap, error) { + values := make(map[string]interface{}, 0) + for k, v := range entryAttributes { + intValues := make([]interface{}, len(v)) + for i, value := range v { + intValues[i] = value + } + values[k] = intValues + } + attributes, err := structpb.NewStruct(values) + if err != nil { + return nil, err + } + return &user.IDPInformation_Ldap{ + Ldap: &user.IDPLDAPAccessInformation{ + Attributes: attributes, + }, + }, nil +} + +func IDPSAMLResponseToPb(assertion []byte) *user.IDPInformation_Saml { + return &user.IDPInformation_Saml{ + Saml: &user.IDPSAMLAccessInformation{ + Assertion: assertion, + }, + } +} + +func (s *Server) checkIntentToken(token string, intentID string) error { + return crypto.CheckToken(s.idpAlg, token, intentID) +} + +func idpUserToAddHumanUser(idpUser idp.User, idpID string) *user.AddHumanUserRequest { + addHumanUser := &user.AddHumanUserRequest{ + Profile: &user.SetHumanProfile{ + GivenName: idpUser.GetFirstName(), + FamilyName: idpUser.GetLastName(), + }, + Email: &user.SetHumanEmail{ + Email: string(idpUser.GetEmail()), + Verification: &user.SetHumanEmail_SendCode{}, + }, + Metadata: make([]*user.SetMetadataEntry, 0), + IdpLinks: []*user.IDPLink{ + { + IdpId: idpID, + UserId: idpUser.GetID(), + UserName: idpUser.GetPreferredUsername(), + }, + }, + } + if username := idpUser.GetPreferredUsername(); username != "" { + addHumanUser.Username = &username + } + if nickName := idpUser.GetNickname(); nickName != "" { + addHumanUser.Profile.NickName = &nickName + } + if displayName := idpUser.GetDisplayName(); displayName != "" { + addHumanUser.Profile.DisplayName = &displayName + } + if lang := idpUser.GetPreferredLanguage().String(); lang != "" { + addHumanUser.Profile.PreferredLanguage = &lang + } + if isEmailVerified := idpUser.IsEmailVerified(); isEmailVerified { + addHumanUser.Email.Verification = &user.SetHumanEmail_IsVerified{IsVerified: isEmailVerified} + } + if phone := idpUser.GetPhone(); phone != "" { + addHumanUser.Phone = &user.SetHumanPhone{ + Phone: string(phone), + Verification: &user.SetHumanPhone_SendCode{}, + } + if isPhoneVerified := idpUser.IsPhoneVerified(); isPhoneVerified { + addHumanUser.Phone.Verification = &user.SetHumanPhone_IsVerified{IsVerified: isPhoneVerified} + } + } + return addHumanUser +} diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index a743206cf0..0f958f0d40 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -2,28 +2,19 @@ package user import ( "context" - "errors" "io" "golang.org/x/text/language" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/idp" - "github.com/zitadel/zitadel/internal/idp/providers/ldap" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" - object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) { - human, err := AddUserRequestToAddHuman(req) if err != nil { return nil, err @@ -356,236 +347,6 @@ func userGrantsToIDs(userGrants []*query.UserGrant) []string { return converted } -func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) { - switch t := req.GetContent().(type) { - case *user.StartIdentityProviderIntentRequest_Urls: - return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls) - case *user.StartIdentityProviderIntentRequest_Ldap: - return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap) - default: - return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderIntent not implemented", t) - } -} - -func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) { - state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID)) - if err != nil { - return nil, err - } - _, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), session.PersistentParameters()) - if err != nil { - return nil, err - } - content, redirect := session.GetAuth(ctx) - if redirect { - return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content}, - }, nil - } - return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_PostForm{ - PostForm: []byte(content), - }, - }, nil -} - -func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) { - intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil) - if err != nil { - return nil, err - } - externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) - if err != nil { - if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil { - return nil, err - } - return nil, err - } - token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes) - if err != nil { - return nil, err - } - return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{ - IdpIntent: &user.IDPIntent{ - IdpIntentId: intentWriteModel.AggregateID, - IdpIntentToken: token, - UserId: userID, - }, - }, - }, nil -} - -func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) { - idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID) - if err != nil { - return "", err - } - externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID) - if err != nil { - return "", err - } - queries := []query.SearchQuery{ - idQuery, externalIDQuery, - } - links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, nil) - if err != nil { - return "", err - } - if len(links.Links) == 1 { - return links.Links[0].UserID, nil - } - return "", nil -} - -func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { - provider, err := s.command.GetProvider(ctx, idpID, "", "") - if err != nil { - return nil, "", nil, err - } - ldapProvider, ok := provider.(*ldap.Provider) - if !ok { - return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "IDP-9a02j2n2bh", "Errors.ExternalIDP.IDPTypeNotImplemented") - } - session := ldapProvider.GetSession(username, password) - externalUser, err := session.FetchUser(ctx) - if errors.Is(err, ldap.ErrFailedLogin) || errors.Is(err, ldap.ErrNoSingleUser) { - return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nzun2i", "Errors.User.ExternalIDP.LoginFailed") - } - if err != nil { - return nil, "", nil, err - } - userID, err := s.checkLinkedExternalUser(ctx, idpID, externalUser.GetID()) - if err != nil { - return nil, "", nil, err - } - - attributes := make(map[string][]string, 0) - for _, item := range session.Entry.Attributes { - attributes[item.Name] = item.Values - } - return externalUser, userID, attributes, nil -} - -func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { - intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), "") - if err != nil { - return nil, err - } - if err := s.checkIntentToken(req.GetIdpIntentToken(), intent.AggregateID); err != nil { - return nil, err - } - if intent.State != domain.IDPIntentStateSucceeded { - return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") - } - return idpIntentToIDPIntentPb(intent, s.idpAlg) -} - -func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { - rawInformation := new(structpb.Struct) - err = rawInformation.UnmarshalJSON(intent.IDPUser) - if err != nil { - return nil, err - } - information := &user.RetrieveIdentityProviderIntentResponse{ - Details: intentToDetailsPb(intent), - IdpInformation: &user.IDPInformation{ - IdpId: intent.IDPID, - UserId: intent.IDPUserID, - UserName: intent.IDPUserName, - RawInformation: rawInformation, - }, - UserId: intent.UserID, - } - if intent.IDPIDToken != "" || intent.IDPAccessToken != nil { - information.IdpInformation.Access, err = idpOAuthTokensToPb(intent.IDPIDToken, intent.IDPAccessToken, alg) - if err != nil { - return nil, err - } - } - - if intent.IDPEntryAttributes != nil { - access, err := IDPEntryAttributesToPb(intent.IDPEntryAttributes) - if err != nil { - return nil, err - } - information.IdpInformation.Access = access - } - - if intent.Assertion != nil { - assertion, err := crypto.Decrypt(intent.Assertion, alg) - if err != nil { - return nil, err - } - information.IdpInformation.Access = IDPSAMLResponseToPb(assertion) - } - - return information, nil -} - -func idpOAuthTokensToPb(idpIDToken string, idpAccessToken *crypto.CryptoValue, alg crypto.EncryptionAlgorithm) (_ *user.IDPInformation_Oauth, err error) { - var idToken *string - if idpIDToken != "" { - idToken = &idpIDToken - } - var accessToken string - if idpAccessToken != nil { - accessToken, err = crypto.DecryptString(idpAccessToken, alg) - if err != nil { - return nil, err - } - } - return &user.IDPInformation_Oauth{ - Oauth: &user.IDPOAuthAccessInformation{ - AccessToken: accessToken, - IdToken: idToken, - }, - }, nil -} - -func intentToDetailsPb(intent *command.IDPIntentWriteModel) *object_pb.Details { - return &object_pb.Details{ - Sequence: intent.ProcessedSequence, - ChangeDate: timestamppb.New(intent.ChangeDate), - ResourceOwner: intent.ResourceOwner, - } -} - -func IDPEntryAttributesToPb(entryAttributes map[string][]string) (*user.IDPInformation_Ldap, error) { - values := make(map[string]interface{}, 0) - for k, v := range entryAttributes { - intValues := make([]interface{}, len(v)) - for i, value := range v { - intValues[i] = value - } - values[k] = intValues - } - attributes, err := structpb.NewStruct(values) - if err != nil { - return nil, err - } - return &user.IDPInformation_Ldap{ - Ldap: &user.IDPLDAPAccessInformation{ - Attributes: attributes, - }, - }, nil -} - -func IDPSAMLResponseToPb(assertion []byte) *user.IDPInformation_Saml { - return &user.IDPInformation_Saml{ - Saml: &user.IDPSAMLAccessInformation{ - Assertion: assertion, - }, - } -} - -func (s *Server) checkIntentToken(token string, intentID string) error { - return crypto.CheckToken(s.idpAlg, token, intentID) -} - func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) { authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.GetUserId(), true, req.GetDomainQuery().GetIncludeWithoutDomain(), req.GetDomainQuery().GetDomain()) if err != nil { diff --git a/internal/api/grpc/user/v2/user_test.go b/internal/api/grpc/user/v2/user_test.go index 9e7a5a5ab0..9408b3acf9 100644 --- a/internal/api/grpc/user/v2/user_test.go +++ b/internal/api/grpc/user/v2/user_test.go @@ -11,7 +11,6 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -322,7 +321,7 @@ func Test_idpIntentToIDPIntentPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := idpIntentToIDPIntentPb(tt.args.intent, tt.args.alg) require.ErrorIs(t, err, tt.res.err) - grpc.AllFieldsEqual(t, tt.res.resp.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) + assert.EqualExportedValues(t, tt.res.resp, got) }) } } diff --git a/internal/api/grpc/user/v2beta/integration_test/user_test.go b/internal/api/grpc/user/v2beta/integration_test/user_test.go index ab2e3215ee..a81de58761 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -2146,17 +2146,29 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { } func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId() - intentID := Instance.CreateIntent(CTX, idpID).GetIdpIntent().GetIdpIntentId() - successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "") + oauthIdpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId() + oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, gofakeit.AppName()).GetId() + samlIdpID := Instance.AddSAMLPostProvider(IamCTX) + ldapIdpID := Instance.AddLDAPProvider(IamCTX) + authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) require.NoError(t, err) - successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "user") + intentID := authURL.Query().Get("state") + + successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "") require.NoError(t, err) - ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "") + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user") require.NoError(t, err) - ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "user") + oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "") require.NoError(t, err) - samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), idpID, "id", "") + oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user") + require.NoError(t, err) + ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "") + require.NoError(t, err) + ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user") + require.NoError(t, err) + samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "") + require.NoError(t, err) + samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user") require.NoError(t, err) type args struct { ctx context.Context @@ -2191,7 +2203,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { wantErr: true, }, { - name: "retrieve successful intent", + name: "retrieve successful oauth intent", args: args{ CTX, &user.RetrieveIdentityProviderIntentRequest{ @@ -2212,13 +2224,15 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdToken: gu.Ptr("idToken"), }, }, - IdpId: idpID, + IdpId: oauthIdpID, UserId: "id", - UserName: "username", + UserName: "", RawInformation: func() *structpb.Struct { s, err := structpb.NewStruct(map[string]interface{}{ - "sub": "id", - "preferred_username": "username", + "RawInfo": map[string]interface{}{ + "id": "id", + "preferred_username": "username", + }, }) require.NoError(t, err) return s @@ -2250,7 +2264,85 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdToken: gu.Ptr("idToken"), }, }, - IdpId: idpID, + IdpId: oauthIdpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "RawInfo": map[string]interface{}{ + "id": "id", + "preferred_username": "username", + }, + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful oidc intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: oidcSuccessful, + IdpIntentToken: oidcToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(oidcChangeDate), + ResourceOwner: Instance.ID(), + Sequence: oidcSequence, + }, + UserId: "", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: oidcIdpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + "preferred_username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful oidc intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: oidcSuccessfulWithUserID, + IdpIntentToken: oidcWithUserIDToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(oidcWithUserIDChangeDate), + ResourceOwner: Instance.ID(), + Sequence: oidcWithUserIDSequence, + }, + UserId: "user", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: oidcIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2294,7 +2386,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }(), }, }, - IdpId: idpID, + IdpId: ldapIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2340,7 +2432,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }(), }, }, - IdpId: idpID, + IdpId: ldapIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2377,7 +2469,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { Assertion: []byte(""), }, }, - IdpId: idpID, + IdpId: samlIdpID, UserId: "id", UserName: "", RawInformation: func() *structpb.Struct { @@ -2394,6 +2486,45 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful saml intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: samlSuccessfulWithUserID, + IdpIntentToken: samlWithUserToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(samlWithUserChangeDate), + ResourceOwner: Instance.ID(), + Sequence: samlWithUserSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Saml{ + Saml: &user.IDPSAMLAccessInformation{ + Assertion: []byte(""), + }, + }, + IdpId: samlIdpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": "id", + "attributes": map[string]interface{}{ + "attribute1": []interface{}{"value1"}, + }, + }) + require.NoError(t, err) + return s + }(), + }, + UserId: "user", + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/grpc/resources/webkey/v3/integration_test/webkey_integration_test.go b/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go similarity index 65% rename from internal/api/grpc/resources/webkey/v3/integration_test/webkey_integration_test.go rename to internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go index eafa733fd1..002669c233 100644 --- a/internal/api/grpc/resources/webkey/v3/integration_test/webkey_integration_test.go +++ b/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go @@ -17,9 +17,7 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" - webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" + webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) var ( @@ -37,7 +35,7 @@ func TestMain(m *testing.M) { func TestServer_Feature_Disabled(t *testing.T) { instance, iamCtx, _ := createInstance(t, false) - client := instance.Client.WebKeyV3Alpha + client := instance.Client.WebKeyV2Beta t.Run("CreateWebKey", func(t *testing.T) { _, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{}) @@ -65,84 +63,78 @@ func TestServer_ListWebKeys(t *testing.T) { instance, iamCtx, creationDate := createInstance(t, true) // After the feature is first enabled, we can expect 2 generated keys with the default config. checkWebKeyListState(iamCtx, t, instance, 2, "", &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, creationDate) } func TestServer_CreateWebKey(t *testing.T) { instance, iamCtx, creationDate := createInstance(t, true) - client := instance.Client.WebKeyV3Alpha + client := instance.Client.WebKeyV2Beta _, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ - Key: &webkey.WebKey{ - Config: &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, - }, + Key: &webkey.CreateWebKeyRequest_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, }) require.NoError(t, err) checkWebKeyListState(iamCtx, t, instance, 3, "", &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, creationDate) } func TestServer_ActivateWebKey(t *testing.T) { instance, iamCtx, creationDate := createInstance(t, true) - client := instance.Client.WebKeyV3Alpha + client := instance.Client.WebKeyV2Beta resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ - Key: &webkey.WebKey{ - Config: &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, - }, + Key: &webkey.CreateWebKeyRequest_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, }) require.NoError(t, err) _, err = client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{ - Id: resp.GetDetails().GetId(), + Id: resp.GetId(), }) require.NoError(t, err) - checkWebKeyListState(iamCtx, t, instance, 3, resp.GetDetails().GetId(), &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + checkWebKeyListState(iamCtx, t, instance, 3, resp.GetId(), &webkey.WebKey_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, creationDate) } func TestServer_DeleteWebKey(t *testing.T) { instance, iamCtx, creationDate := createInstance(t, true) - client := instance.Client.WebKeyV3Alpha + client := instance.Client.WebKeyV2Beta keyIDs := make([]string, 2) for i := 0; i < 2; i++ { resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ - Key: &webkey.WebKey{ - Config: &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, - }, + Key: &webkey.CreateWebKeyRequest_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, }) require.NoError(t, err) - keyIDs[i] = resp.GetDetails().GetId() + keyIDs[i] = resp.GetId() } _, err := client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{ Id: keyIDs[0], @@ -162,11 +154,35 @@ func TestServer_DeleteWebKey(t *testing.T) { return } + start := time.Now() ok = t.Run("delete inactive key", func(t *testing.T) { - _, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ + resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ Id: keyIDs[1], }) require.NoError(t, err) + require.WithinRange(t, resp.GetDeletionDate().AsTime(), start, time.Now()) + }) + if !ok { + return + } + + ok = t.Run("delete inactive key again", func(t *testing.T) { + resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ + Id: keyIDs[1], + }) + require.NoError(t, err) + require.WithinRange(t, resp.GetDeletionDate().AsTime(), start, time.Now()) + }) + if !ok { + return + } + + ok = t.Run("delete not existing key", func(t *testing.T) { + resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ + Id: "not-existing", + }) + require.NoError(t, err) + require.Nil(t, resp.DeletionDate) }) if !ok { return @@ -174,9 +190,9 @@ func TestServer_DeleteWebKey(t *testing.T) { // There are 2 keys from feature setup, +2 created, -1 deleted = 3 checkWebKeyListState(iamCtx, t, instance, 3, keyIDs[0], &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, creationDate) } @@ -195,7 +211,7 @@ func createInstance(t *testing.T, enableFeature bool) (*integration.Instance, co retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamCTX, time.Minute) assert.EventuallyWithT(t, func(collect *assert.CollectT) { - resp, err := instance.Client.WebKeyV3Alpha.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{}) + resp, err := instance.Client.WebKeyV2Beta.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{}) if enableFeature { assert.NoError(collect, err) assert.Len(collect, resp.GetWebKeys(), 2) @@ -220,7 +236,7 @@ func checkWebKeyListState(ctx context.Context, t *testing.T, instance *integrati retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) assert.EventuallyWithT(t, func(collect *assert.CollectT) { - resp, err := instance.Client.WebKeyV3Alpha.ListWebKeys(ctx, &webkey.ListWebKeysRequest{}) + resp, err := instance.Client.WebKeyV2Beta.ListWebKeys(ctx, &webkey.ListWebKeysRequest{}) require.NoError(collect, err) list := resp.GetWebKeys() assert.Len(collect, list, nKeys) @@ -228,21 +244,14 @@ func checkWebKeyListState(ctx context.Context, t *testing.T, instance *integrati now := time.Now() var gotActiveKeyID string for _, key := range list { - integration.AssertResourceDetails(t, &resource_object.Details{ - Created: creationDate, - Changed: creationDate, - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, key.GetDetails()) - assert.WithinRange(collect, key.GetDetails().GetChanged().AsTime(), now.Add(-time.Minute), now.Add(time.Minute)) - assert.NotEqual(collect, webkey.WebKeyState_STATE_UNSPECIFIED, key.GetState()) - assert.NotEqual(collect, webkey.WebKeyState_STATE_REMOVED, key.GetState()) - assert.Equal(collect, config, key.GetConfig().GetConfig()) + assert.WithinRange(collect, key.GetCreationDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute)) + assert.WithinRange(collect, key.GetChangeDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute)) + assert.NotEqual(collect, webkey.State_STATE_UNSPECIFIED, key.GetState()) + assert.NotEqual(collect, webkey.State_STATE_REMOVED, key.GetState()) + assert.Equal(collect, config, key.GetKey()) - if key.GetState() == webkey.WebKeyState_STATE_ACTIVE { - gotActiveKeyID = key.GetDetails().GetId() + if key.GetState() == webkey.State_STATE_ACTIVE { + gotActiveKeyID = key.GetId() } } assert.NotEmpty(collect, gotActiveKeyID) diff --git a/internal/api/grpc/resources/webkey/v3/server.go b/internal/api/grpc/webkey/v2beta/server.go similarity index 67% rename from internal/api/grpc/resources/webkey/v3/server.go rename to internal/api/grpc/webkey/v2beta/server.go index 4e97965932..0d4ddb19c8 100644 --- a/internal/api/grpc/resources/webkey/v3/server.go +++ b/internal/api/grpc/webkey/v2beta/server.go @@ -7,11 +7,11 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" - webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" + webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) type Server struct { - webkey.UnimplementedZITADELWebKeysServer + webkey.UnimplementedWebKeyServiceServer command *command.Commands query *query.Queries } @@ -27,21 +27,21 @@ func CreateServer( } func (s *Server) RegisterServer(grpcServer *grpc.Server) { - webkey.RegisterZITADELWebKeysServer(grpcServer, s) + webkey.RegisterWebKeyServiceServer(grpcServer, s) } func (s *Server) AppName() string { - return webkey.ZITADELWebKeys_ServiceDesc.ServiceName + return webkey.WebKeyService_ServiceDesc.ServiceName } func (s *Server) MethodPrefix() string { - return webkey.ZITADELWebKeys_ServiceDesc.ServiceName + return webkey.WebKeyService_ServiceDesc.ServiceName } func (s *Server) AuthMethods() authz.MethodMapping { - return webkey.ZITADELWebKeys_AuthMethods + return webkey.WebKeyService_AuthMethods } func (s *Server) RegisterGateway() server.RegisterGatewayFunc { - return webkey.RegisterZITADELWebKeysHandler + return webkey.RegisterWebKeyServiceHandler } diff --git a/internal/api/grpc/resources/webkey/v3/webkey.go b/internal/api/grpc/webkey/v2beta/webkey.go similarity index 72% rename from internal/api/grpc/resources/webkey/v3/webkey.go rename to internal/api/grpc/webkey/v2beta/webkey.go index 8a6e72f950..d45288dff2 100644 --- a/internal/api/grpc/resources/webkey/v3/webkey.go +++ b/internal/api/grpc/webkey/v2beta/webkey.go @@ -3,12 +3,12 @@ package webkey import ( "context" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/api/authz" - resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" + webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyRequest) (_ *webkey.CreateWebKeyResponse, err error) { @@ -24,7 +24,8 @@ func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyReque } return &webkey.CreateWebKeyResponse{ - Details: resource_object.DomainToDetailsPb(webKey.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()), + Id: webKey.KeyID, + CreationDate: timestamppb.New(webKey.ObjectDetails.EventDate), }, nil } @@ -41,7 +42,7 @@ func (s *Server) ActivateWebKey(ctx context.Context, req *webkey.ActivateWebKeyR } return &webkey.ActivateWebKeyResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()), + ChangeDate: timestamppb.New(details.EventDate), }, nil } @@ -52,13 +53,17 @@ func (s *Server) DeleteWebKey(ctx context.Context, req *webkey.DeleteWebKeyReque if err = checkWebKeyFeature(ctx); err != nil { return nil, err } - details, err := s.command.DeleteWebKey(ctx, req.GetId()) + deletedAt, err := s.command.DeleteWebKey(ctx, req.GetId()) if err != nil { return nil, err } + var deletionDate *timestamppb.Timestamp + if !deletedAt.IsZero() { + deletionDate = timestamppb.New(deletedAt) + } return &webkey.DeleteWebKeyResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()), + DeletionDate: deletionDate, }, nil } @@ -75,7 +80,7 @@ func (s *Server) ListWebKeys(ctx context.Context, _ *webkey.ListWebKeysRequest) } return &webkey.ListWebKeysResponse{ - WebKeys: webKeyDetailsListToPb(list, authz.GetInstance(ctx).InstanceID()), + WebKeys: webKeyDetailsListToPb(list), }, nil } diff --git a/internal/api/grpc/webkey/v2beta/webkey_converter.go b/internal/api/grpc/webkey/v2beta/webkey_converter.go new file mode 100644 index 0000000000..ac8939470b --- /dev/null +++ b/internal/api/grpc/webkey/v2beta/webkey_converter.go @@ -0,0 +1,170 @@ +package webkey + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" +) + +func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig { + switch config := req.GetKey().(type) { + case *webkey.CreateWebKeyRequest_Rsa: + return rsaToCrypto(config.Rsa) + case *webkey.CreateWebKeyRequest_Ecdsa: + return ecdsaToCrypto(config.Ecdsa) + case *webkey.CreateWebKeyRequest_Ed25519: + return new(crypto.WebKeyED25519Config) + default: + return rsaToCrypto(nil) + } +} + +func rsaToCrypto(config *webkey.RSA) *crypto.WebKeyRSAConfig { + out := new(crypto.WebKeyRSAConfig) + + switch config.GetBits() { + case webkey.RSABits_RSA_BITS_UNSPECIFIED: + out.Bits = crypto.RSABits2048 + case webkey.RSABits_RSA_BITS_2048: + out.Bits = crypto.RSABits2048 + case webkey.RSABits_RSA_BITS_3072: + out.Bits = crypto.RSABits3072 + case webkey.RSABits_RSA_BITS_4096: + out.Bits = crypto.RSABits4096 + default: + out.Bits = crypto.RSABits2048 + } + + switch config.GetHasher() { + case webkey.RSAHasher_RSA_HASHER_UNSPECIFIED: + out.Hasher = crypto.RSAHasherSHA256 + case webkey.RSAHasher_RSA_HASHER_SHA256: + out.Hasher = crypto.RSAHasherSHA256 + case webkey.RSAHasher_RSA_HASHER_SHA384: + out.Hasher = crypto.RSAHasherSHA384 + case webkey.RSAHasher_RSA_HASHER_SHA512: + out.Hasher = crypto.RSAHasherSHA512 + default: + out.Hasher = crypto.RSAHasherSHA256 + } + + return out +} + +func ecdsaToCrypto(config *webkey.ECDSA) *crypto.WebKeyECDSAConfig { + out := new(crypto.WebKeyECDSAConfig) + + switch config.GetCurve() { + case webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED: + out.Curve = crypto.EllipticCurveP256 + case webkey.ECDSACurve_ECDSA_CURVE_P256: + out.Curve = crypto.EllipticCurveP256 + case webkey.ECDSACurve_ECDSA_CURVE_P384: + out.Curve = crypto.EllipticCurveP384 + case webkey.ECDSACurve_ECDSA_CURVE_P512: + out.Curve = crypto.EllipticCurveP512 + default: + out.Curve = crypto.EllipticCurveP256 + } + + return out +} + +func webKeyDetailsListToPb(list []query.WebKeyDetails) []*webkey.WebKey { + out := make([]*webkey.WebKey, len(list)) + for i := range list { + out[i] = webKeyDetailsToPb(&list[i]) + } + return out +} + +func webKeyDetailsToPb(details *query.WebKeyDetails) *webkey.WebKey { + out := &webkey.WebKey{ + Id: details.KeyID, + CreationDate: timestamppb.New(details.CreationDate), + ChangeDate: timestamppb.New(details.ChangeDate), + State: webKeyStateToPb(details.State), + } + + switch config := details.Config.(type) { + case *crypto.WebKeyRSAConfig: + out.Key = &webkey.WebKey_Rsa{ + Rsa: webKeyRSAConfigToPb(config), + } + case *crypto.WebKeyECDSAConfig: + out.Key = &webkey.WebKey_Ecdsa{ + Ecdsa: webKeyECDSAConfigToPb(config), + } + case *crypto.WebKeyED25519Config: + out.Key = &webkey.WebKey_Ed25519{ + Ed25519: new(webkey.ED25519), + } + } + + return out +} + +func webKeyStateToPb(state domain.WebKeyState) webkey.State { + switch state { + case domain.WebKeyStateUnspecified: + return webkey.State_STATE_UNSPECIFIED + case domain.WebKeyStateInitial: + return webkey.State_STATE_INITIAL + case domain.WebKeyStateActive: + return webkey.State_STATE_ACTIVE + case domain.WebKeyStateInactive: + return webkey.State_STATE_INACTIVE + case domain.WebKeyStateRemoved: + return webkey.State_STATE_REMOVED + default: + return webkey.State_STATE_UNSPECIFIED + } +} + +func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.RSA { + out := new(webkey.RSA) + + switch config.Bits { + case crypto.RSABitsUnspecified: + out.Bits = webkey.RSABits_RSA_BITS_UNSPECIFIED + case crypto.RSABits2048: + out.Bits = webkey.RSABits_RSA_BITS_2048 + case crypto.RSABits3072: + out.Bits = webkey.RSABits_RSA_BITS_3072 + case crypto.RSABits4096: + out.Bits = webkey.RSABits_RSA_BITS_4096 + } + + switch config.Hasher { + case crypto.RSAHasherUnspecified: + out.Hasher = webkey.RSAHasher_RSA_HASHER_UNSPECIFIED + case crypto.RSAHasherSHA256: + out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA256 + case crypto.RSAHasherSHA384: + out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA384 + case crypto.RSAHasherSHA512: + out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA512 + } + + return out +} + +func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.ECDSA { + out := new(webkey.ECDSA) + + switch config.Curve { + case crypto.EllipticCurveUnspecified: + out.Curve = webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED + case crypto.EllipticCurveP256: + out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P256 + case crypto.EllipticCurveP384: + out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P384 + case crypto.EllipticCurveP512: + out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P512 + } + + return out +} diff --git a/internal/api/grpc/resources/webkey/v3/webkey_converter_test.go b/internal/api/grpc/webkey/v2beta/webkey_converter_test.go similarity index 56% rename from internal/api/grpc/resources/webkey/v3/webkey_converter_test.go rename to internal/api/grpc/webkey/v2beta/webkey_converter_test.go index e755d2be08..d78e9968dc 100644 --- a/internal/api/grpc/resources/webkey/v3/webkey_converter_test.go +++ b/internal/api/grpc/webkey/v2beta/webkey_converter_test.go @@ -10,9 +10,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" - webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" + webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) func Test_createWebKeyRequestToConfig(t *testing.T) { @@ -27,12 +25,10 @@ func Test_createWebKeyRequestToConfig(t *testing.T) { { name: "RSA", args: args{&webkey.CreateWebKeyRequest{ - Key: &webkey.WebKey{ - Config: &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, - }, + Key: &webkey.CreateWebKeyRequest_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, }, }, }}, @@ -44,11 +40,9 @@ func Test_createWebKeyRequestToConfig(t *testing.T) { { name: "ECDSA", args: args{&webkey.CreateWebKeyRequest{ - Key: &webkey.WebKey{ - Config: &webkey.WebKey_Ecdsa{ - Ecdsa: &webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384, - }, + Key: &webkey.CreateWebKeyRequest_Ecdsa{ + Ecdsa: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P384, }, }, }}, @@ -59,10 +53,8 @@ func Test_createWebKeyRequestToConfig(t *testing.T) { { name: "ED25519", args: args{&webkey.CreateWebKeyRequest{ - Key: &webkey.WebKey{ - Config: &webkey.WebKey_Ed25519{ - Ed25519: &webkey.WebKeyED25519Config{}, - }, + Key: &webkey.CreateWebKeyRequest_Ed25519{ + Ed25519: &webkey.ED25519{}, }, }}, want: &crypto.WebKeyED25519Config{}, @@ -86,7 +78,7 @@ func Test_createWebKeyRequestToConfig(t *testing.T) { func Test_webKeyRSAConfigToCrypto(t *testing.T) { type args struct { - config *webkey.WebKeyRSAConfig + config *webkey.RSA } tests := []struct { name string @@ -95,9 +87,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { }{ { name: "unspecified", - args: args{&webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED, + args: args{&webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_UNSPECIFIED, + Hasher: webkey.RSAHasher_RSA_HASHER_UNSPECIFIED, }}, want: &crypto.WebKeyRSAConfig{ Bits: crypto.RSABits2048, @@ -106,9 +98,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { }, { name: "2048, RSA256", - args: args{&webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + args: args{&webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }}, want: &crypto.WebKeyRSAConfig{ Bits: crypto.RSABits2048, @@ -117,9 +109,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { }, { name: "3072, RSA384", - args: args{&webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, + args: args{&webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, }}, want: &crypto.WebKeyRSAConfig{ Bits: crypto.RSABits3072, @@ -128,9 +120,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { }, { name: "4096, RSA512", - args: args{&webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512, + args: args{&webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_4096, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA512, }}, want: &crypto.WebKeyRSAConfig{ Bits: crypto.RSABits4096, @@ -139,7 +131,7 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { }, { name: "invalid", - args: args{&webkey.WebKeyRSAConfig{ + args: args{&webkey.RSA{ Bits: 99, Hasher: 99, }}, @@ -151,7 +143,7 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := webKeyRSAConfigToCrypto(tt.args.config) + got := rsaToCrypto(tt.args.config) assert.Equal(t, tt.want, got) }) } @@ -159,7 +151,7 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { func Test_webKeyECDSAConfigToCrypto(t *testing.T) { type args struct { - config *webkey.WebKeyECDSAConfig + config *webkey.ECDSA } tests := []struct { name string @@ -168,8 +160,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) { }{ { name: "unspecified", - args: args{&webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED, + args: args{&webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED, }}, want: &crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP256, @@ -177,8 +169,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) { }, { name: "P256", - args: args{&webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256, + args: args{&webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P256, }}, want: &crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP256, @@ -186,8 +178,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) { }, { name: "P384", - args: args{&webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384, + args: args{&webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P384, }}, want: &crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP384, @@ -195,8 +187,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) { }, { name: "P512", - args: args{&webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512, + args: args{&webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P512, }}, want: &crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP512, @@ -204,7 +196,7 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) { }, { name: "invalid", - args: args{&webkey.WebKeyECDSAConfig{ + args: args{&webkey.ECDSA{ Curve: 99, }}, want: &crypto.WebKeyECDSAConfig{ @@ -214,14 +206,13 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := webKeyECDSAConfigToCrypto(tt.args.config) + got := ecdsaToCrypto(tt.args.config) assert.Equal(t, tt.want, got) }) } } func Test_webKeyDetailsListToPb(t *testing.T) { - instanceID := "ownerid" list := []query.WebKeyDetails{ { KeyID: "key1", @@ -243,52 +234,41 @@ func Test_webKeyDetailsListToPb(t *testing.T) { Config: &crypto.WebKeyED25519Config{}, }, } - want := []*webkey.GetWebKey{ + want := []*webkey.WebKey{ { - Details: &resource_object.Details{ - Id: "key1", - Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, - Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, - }, - State: webkey.WebKeyState_STATE_ACTIVE, - Config: &webkey.WebKey{ - Config: &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, - }, + Id: "key1", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, }, }, }, { - Details: &resource_object.Details{ - Id: "key2", - Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, - Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, - }, - State: webkey.WebKeyState_STATE_ACTIVE, - Config: &webkey.WebKey{ - Config: &webkey.WebKey_Ed25519{ - Ed25519: &webkey.WebKeyED25519Config{}, - }, + Id: "key2", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Ed25519{ + Ed25519: &webkey.ED25519{}, }, }, } - got := webKeyDetailsListToPb(list, instanceID) + got := webKeyDetailsListToPb(list) assert.Equal(t, want, got) } func Test_webKeyDetailsToPb(t *testing.T) { - instanceID := "ownerid" type args struct { details *query.WebKeyDetails } tests := []struct { name string args args - want *webkey.GetWebKey + want *webkey.WebKey }{ { name: "RSA", @@ -303,20 +283,15 @@ func Test_webKeyDetailsToPb(t *testing.T) { Hasher: crypto.RSAHasherSHA384, }, }}, - want: &webkey.GetWebKey{ - Details: &resource_object.Details{ - Id: "keyID", - Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, - Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, - }, - State: webkey.WebKeyState_STATE_ACTIVE, - Config: &webkey.WebKey{ - Config: &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, - }, + want: &webkey.WebKey{ + Id: "keyID", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, }, }, }, @@ -333,19 +308,14 @@ func Test_webKeyDetailsToPb(t *testing.T) { Curve: crypto.EllipticCurveP384, }, }}, - want: &webkey.GetWebKey{ - Details: &resource_object.Details{ - Id: "keyID", - Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, - Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, - }, - State: webkey.WebKeyState_STATE_ACTIVE, - Config: &webkey.WebKey{ - Config: &webkey.WebKey_Ecdsa{ - Ecdsa: &webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384, - }, + want: &webkey.WebKey{ + Id: "keyID", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Ecdsa{ + Ecdsa: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P384, }, }, }, @@ -360,25 +330,20 @@ func Test_webKeyDetailsToPb(t *testing.T) { State: domain.WebKeyStateActive, Config: &crypto.WebKeyED25519Config{}, }}, - want: &webkey.GetWebKey{ - Details: &resource_object.Details{ - Id: "keyID", - Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, - Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, - }, - State: webkey.WebKeyState_STATE_ACTIVE, - Config: &webkey.WebKey{ - Config: &webkey.WebKey_Ed25519{ - Ed25519: &webkey.WebKeyED25519Config{}, - }, + want: &webkey.WebKey{ + Id: "keyID", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Ed25519{ + Ed25519: &webkey.ED25519{}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := webKeyDetailsToPb(tt.args.details, instanceID) + got := webKeyDetailsToPb(tt.args.details) assert.Equal(t, tt.want, got) }) } @@ -391,37 +356,37 @@ func Test_webKeyStateToPb(t *testing.T) { tests := []struct { name string args args - want webkey.WebKeyState + want webkey.State }{ { name: "unspecified", args: args{domain.WebKeyStateUnspecified}, - want: webkey.WebKeyState_STATE_UNSPECIFIED, + want: webkey.State_STATE_UNSPECIFIED, }, { name: "initial", args: args{domain.WebKeyStateInitial}, - want: webkey.WebKeyState_STATE_INITIAL, + want: webkey.State_STATE_INITIAL, }, { name: "active", args: args{domain.WebKeyStateActive}, - want: webkey.WebKeyState_STATE_ACTIVE, + want: webkey.State_STATE_ACTIVE, }, { name: "inactive", args: args{domain.WebKeyStateInactive}, - want: webkey.WebKeyState_STATE_INACTIVE, + want: webkey.State_STATE_INACTIVE, }, { name: "removed", args: args{domain.WebKeyStateRemoved}, - want: webkey.WebKeyState_STATE_REMOVED, + want: webkey.State_STATE_REMOVED, }, { name: "invalid", args: args{99}, - want: webkey.WebKeyState_STATE_UNSPECIFIED, + want: webkey.State_STATE_UNSPECIFIED, }, } for _, tt := range tests { @@ -439,7 +404,7 @@ func Test_webKeyRSAConfigToPb(t *testing.T) { tests := []struct { name string args args - want *webkey.WebKeyRSAConfig + want *webkey.RSA }{ { name: "2048, RSA256", @@ -447,9 +412,9 @@ func Test_webKeyRSAConfigToPb(t *testing.T) { Bits: crypto.RSABits2048, Hasher: crypto.RSAHasherSHA256, }}, - want: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + want: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, { @@ -458,9 +423,9 @@ func Test_webKeyRSAConfigToPb(t *testing.T) { Bits: crypto.RSABits3072, Hasher: crypto.RSAHasherSHA384, }}, - want: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, + want: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, }, }, { @@ -469,9 +434,9 @@ func Test_webKeyRSAConfigToPb(t *testing.T) { Bits: crypto.RSABits4096, Hasher: crypto.RSAHasherSHA512, }}, - want: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512, + want: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_4096, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA512, }, }, } @@ -490,15 +455,15 @@ func Test_webKeyECDSAConfigToPb(t *testing.T) { tests := []struct { name string args args - want *webkey.WebKeyECDSAConfig + want *webkey.ECDSA }{ { name: "P256", args: args{&crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP256, }}, - want: &webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256, + want: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P256, }, }, { @@ -506,8 +471,8 @@ func Test_webKeyECDSAConfigToPb(t *testing.T) { args: args{&crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP384, }}, - want: &webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384, + want: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P384, }, }, { @@ -515,8 +480,8 @@ func Test_webKeyECDSAConfigToPb(t *testing.T) { args: args{&crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP512, }}, - want: &webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512, + want: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P512, }, }, } diff --git a/internal/api/http/middleware/auth_interceptor.go b/internal/api/http/middleware/auth_interceptor.go index 1581d401b4..ae9377b13d 100644 --- a/internal/api/http/middleware/auth_interceptor.go +++ b/internal/api/http/middleware/auth_interceptor.go @@ -14,14 +14,16 @@ import ( ) type AuthInterceptor struct { - verifier authz.APITokenVerifier - authConfig authz.Config + verifier authz.APITokenVerifier + authConfig authz.Config + systemAuthConfig authz.Config } -func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) *AuthInterceptor { +func AuthorizationInterceptor(verifier authz.APITokenVerifier, systemAuthConfig authz.Config, authConfig authz.Config) *AuthInterceptor { return &AuthInterceptor{ - verifier: verifier, - authConfig: authConfig, + verifier: verifier, + authConfig: authConfig, + systemAuthConfig: systemAuthConfig, } } @@ -31,7 +33,7 @@ func (a *AuthInterceptor) Handler(next http.Handler) http.Handler { func (a *AuthInterceptor) HandlerFunc(next http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, err := authorize(r, a.verifier, a.authConfig) + ctx, err := authorize(r, a.verifier, a.systemAuthConfig, a.authConfig) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return @@ -44,7 +46,7 @@ func (a *AuthInterceptor) HandlerFunc(next http.Handler) http.HandlerFunc { func (a *AuthInterceptor) HandlerFuncWithError(next HandlerFuncWithError) HandlerFuncWithError { return func(w http.ResponseWriter, r *http.Request) error { - ctx, err := authorize(r, a.verifier, a.authConfig) + ctx, err := authorize(r, a.verifier, a.systemAuthConfig, a.authConfig) if err != nil { return err } @@ -56,7 +58,7 @@ func (a *AuthInterceptor) HandlerFuncWithError(next HandlerFuncWithError) Handle type httpReq struct{} -func authorize(r *http.Request, verifier authz.APITokenVerifier, authConfig authz.Config) (_ context.Context, err error) { +func authorize(r *http.Request, verifier authz.APITokenVerifier, systemAuthConfig authz.Config, authConfig authz.Config) (_ context.Context, err error) { ctx := r.Context() authOpt, needsToken := checkAuthMethod(r, verifier) @@ -71,7 +73,7 @@ func authorize(r *http.Request, verifier authz.APITokenVerifier, authConfig auth return nil, zerrors.ThrowUnauthenticated(nil, "AUT-1179", "auth header missing") } - ctxSetter, err := authz.CheckUserAuthorization(authCtx, &httpReq{}, authToken, http_util.GetOrgID(r), "", verifier, authConfig, authOpt, r.RequestURI) + ctxSetter, err := authz.CheckUserAuthorization(authCtx, &httpReq{}, authToken, http_util.GetOrgID(r), "", verifier, systemAuthConfig.RolePermissionMappings, authConfig.RolePermissionMappings, authOpt, r.RequestURI) if err != nil { return nil, err } diff --git a/internal/api/http/middleware/middleware_test.go b/internal/api/http/middleware/middleware_test.go index 4d7cb6636d..60d4099e06 100644 --- a/internal/api/http/middleware/middleware_test.go +++ b/internal/api/http/middleware/middleware_test.go @@ -1,6 +1,7 @@ package middleware import ( + "os" "testing" "golang.org/x/text/language" @@ -14,5 +15,5 @@ var ( func TestMain(m *testing.M) { i18n.SupportLanguages(SupportedLanguages...) - m.Run() + os.Exit(m.Run()) } diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index d433603cd8..f750b2a3ea 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -111,6 +111,7 @@ func (o *OPStorage) createAuthRequestLoginClient(ctx context.Context, req *oidc. Prompt: PromptToBusiness(req.Prompt), UILocales: UILocalesToBusiness(req.UILocales), MaxAge: MaxAgeToBusiness(req.MaxAge), + Issuer: o.contextToIssuer(ctx), } if req.LoginHint != "" { authRequest.LoginHint = &req.LoginHint @@ -149,7 +150,7 @@ func (o *OPStorage) audienceFromProjectID(ctx context.Context, projectID string) if err != nil { return nil, err } - appIDs, err := o.query.SearchClientIDs(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, true) + appIDs, err := o.query.SearchClientIDs(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, false) if err != nil { return nil, err } @@ -545,11 +546,7 @@ func CreateCodeCallbackURL(ctx context.Context, authReq op.AuthRequest, authoriz code: code, state: authReq.GetState(), } - callback, err := op.AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder()) - if err != nil { - return "", err - } - return callback, err + return op.AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder()) } func (s *Server) CreateTokenCallbackURL(ctx context.Context, req op.AuthRequest) (string, error) { diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 76f78ab5ab..81f3b1c466 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -417,7 +417,6 @@ func (o *OPStorage) getMaxKeySequence(ctx context.Context) (float64, error) { eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). ResourceOwner(authz.GetInstance(ctx).InstanceID()). AwaitOpenTransactions(). - AllowTimeTravel(). AddQuery(). AggregateTypes( keypair.AggregateType, diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index 153a13f06e..37a9ba2bce 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -75,6 +75,7 @@ type OPStorage struct { encAlg crypto.EncryptionAlgorithm locker crdb.Locker assetAPIPrefix func(ctx context.Context) string + contextToIssuer func(context.Context) string } // Provider is used to overload certain [op.Provider] methods @@ -119,7 +120,7 @@ func NewServer( if err != nil { return nil, zerrors.ThrowInternal(err, "OIDC-EGrqd", "cannot create op config: %w") } - storage := newStorage(config, command, query, repo, encryptionAlg, es, projections) + storage := newStorage(config, command, query, repo, encryptionAlg, es, projections, ContextToIssuer) keyCache := newPublicKeyCache(ctx, config.PublicKeyCacheMaxAge, queryKeyFunc(query)) accessTokenKeySet := newOidcKeySet(keyCache, withKeyExpiryCheck(true)) idTokenHintKeySet := newOidcKeySet(keyCache) @@ -182,9 +183,13 @@ func NewServer( return server, nil } +func ContextToIssuer(ctx context.Context) string { + return http_utils.DomainContext(ctx).Origin() +} + func IssuerFromContext(_ bool) (op.IssuerFromRequest, error) { return func(r *http.Request) string { - return http_utils.DomainContext(r.Context()).Origin() + return ContextToIssuer(r.Context()) }, nil } @@ -220,7 +225,7 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey [] return opConfig, nil } -func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, db *database.DB) *OPStorage { +func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, db *database.DB, contextToIssuer func(context.Context) string) *OPStorage { return &OPStorage{ repo: repo, command: command, @@ -236,6 +241,7 @@ func newStorage(config Config, command *command.Commands, query *query.Queries, encAlg: encAlg, locker: crdb.NewLocker(db.DB, locksTable, signingKey), assetAPIPrefix: assets.AssetAPI(), + contextToIssuer: contextToIssuer, } } diff --git a/internal/api/oidc/userinfo.go b/internal/api/oidc/userinfo.go index b2121a73a2..61f03b6d0f 100644 --- a/internal/api/oidc/userinfo.go +++ b/internal/api/oidc/userinfo.go @@ -20,7 +20,9 @@ import ( "github.com/zitadel/zitadel/internal/actions/object" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/query" + exec_repo "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -410,5 +412,104 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user } } + var function string + switch triggerType { + case domain.TriggerTypePreUserinfoCreation: + function = exec_repo.ID(domain.ExecutionTypeFunction, domain.ActionFunctionPreUserinfo.LocalizationKey()) + case domain.TriggerTypePreAccessTokenCreation: + function = exec_repo.ID(domain.ExecutionTypeFunction, domain.ActionFunctionPreAccessToken.LocalizationKey()) + case domain.TriggerTypeUnspecified, domain.TriggerTypePostAuthentication, domain.TriggerTypePreCreation, domain.TriggerTypePostCreation, domain.TriggerTypePreSAMLResponseCreation: + // added for linting, there should never be any trigger type be used here besides PreUserinfo and PreAccessToken + return err + } + + if function == "" { + return nil + } + executionTargets, err := execution.QueryExecutionTargetsForFunction(ctx, s.query, function) + if err != nil { + return err + } + info := &ContextInfo{ + Function: function, + UserInfo: userInfo, + User: qu.User, + UserMetadata: qu.Metadata, + Org: qu.Org, + UserGrants: qu.UserGrants, + } + + resp, err := execution.CallTargets(ctx, executionTargets, info) + if err != nil { + return err + } + contextInfoResponse, ok := resp.(*ContextInfoResponse) + if !ok || contextInfoResponse == nil { + return nil + } + claimLogs := make([]string, 0) + for _, metadata := range contextInfoResponse.SetUserMetadata { + if _, err = s.command.SetUserMetadata(ctx, metadata, userInfo.Subject, qu.User.ResourceOwner); err != nil { + claimLogs = append(claimLogs, fmt.Sprintf("failed to set user metadata key %q", metadata.Key)) + } + } + for _, claim := range contextInfoResponse.AppendClaims { + if strings.HasPrefix(claim.Key, ClaimPrefix) { + continue + } + if userInfo.Claims[claim.Key] == nil { + userInfo.AppendClaims(claim.Key, claim.Value) + continue + } + claimLogs = append(claimLogs, fmt.Sprintf("key %q already exists", claim.Key)) + } + claimLogs = append(claimLogs, contextInfoResponse.AppendLogClaims...) + if len(claimLogs) > 0 { + userInfo.AppendClaims(fmt.Sprintf(ClaimActionLogFormat, function), claimLogs) + } + return nil } + +type ContextInfo struct { + Function string `json:"function,omitempty"` + UserInfo *oidc.UserInfo `json:"userinfo,omitempty"` + User *query.User `json:"user,omitempty"` + UserMetadata []query.UserMetadata `json:"user_metadata,omitempty"` + Org *query.UserInfoOrg `json:"org,omitempty"` + UserGrants []query.UserGrant `json:"user_grants,omitempty"` + Response *ContextInfoResponse `json:"response,omitempty"` +} + +type ContextInfoResponse struct { + SetUserMetadata []*domain.Metadata `json:"set_user_metadata,omitempty"` + AppendClaims []*AppendClaim `json:"append_claims,omitempty"` + AppendLogClaims []string `json:"append_log_claims,omitempty"` +} + +type AppendClaim struct { + Key string `json:"key"` + Value any `json:"value"` +} + +func (c *ContextInfo) GetHTTPRequestBody() []byte { + data, err := json.Marshal(c) + if err != nil { + return nil + } + return data +} + +func (c *ContextInfo) SetHTTPResponseBody(resp []byte) error { + if !json.Valid(resp) { + return zerrors.ThrowPreconditionFailed(nil, "ACTION-4m9s2", "Errors.Execution.ResponseIsNotValidJSON") + } + if c.Response == nil { + c.Response = &ContextInfoResponse{} + } + return json.Unmarshal(resp, c.Response) +} + +func (c *ContextInfo) GetContent() any { + return c.Response +} diff --git a/internal/api/saml/auth_request.go b/internal/api/saml/auth_request.go index a846cd090b..db0c74a931 100644 --- a/internal/api/saml/auth_request.go +++ b/internal/api/saml/auth_request.go @@ -32,9 +32,10 @@ func (p *Provider) CreateResponse(ctx context.Context, authReq models.AuthReques RelayState: authReq.GetRelayState(), AcsUrl: authReq.GetAccessConsumerServiceURL(), RequestID: authReq.GetAuthRequestID(), - Issuer: authReq.GetDestination(), Audience: authReq.GetIssuer(), + Issuer: p.GetEntityID(ctx), } + samlResponse, err := p.AuthCallbackResponse(ctx, authReq, resp) if err != nil { return "", "", err diff --git a/internal/api/saml/provider.go b/internal/api/saml/provider.go index edf713456c..428fc35ed9 100644 --- a/internal/api/saml/provider.go +++ b/internal/api/saml/provider.go @@ -1,6 +1,7 @@ package saml import ( + "context" "fmt" "net/http" @@ -59,6 +60,7 @@ func NewProvider( projections, fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID), conf.DefaultLoginURLV2, + ContextToIssuer, ) if err != nil { return nil, err @@ -83,7 +85,7 @@ func NewProvider( p, err := provider.NewProvider( provStorage, - HandlerPrefix, + IssuerFromContext, conf.ProviderConfig, options..., ) @@ -96,6 +98,16 @@ func NewProvider( }, nil } +func ContextToIssuer(ctx context.Context) string { + return http_utils.DomainContext(ctx).Origin() + HandlerPrefix +} + +func IssuerFromContext(_ bool) (provider.IssuerFromRequest, error) { + return func(r *http.Request) string { + return ContextToIssuer(r.Context()) + }, nil +} + func newStorage( command *command.Commands, query *query.Queries, @@ -106,6 +118,7 @@ func newStorage( db *database.DB, defaultLoginURL string, defaultLoginURLV2 string, + contextToIssuer func(context.Context) string, ) (*Storage, error) { return &Storage{ encAlg: encAlg, @@ -117,6 +130,7 @@ func newStorage( query: query, defaultLoginURL: defaultLoginURL, defaultLoginURLv2: defaultLoginURLV2, + contextToIssuer: contextToIssuer, }, nil } diff --git a/internal/api/saml/storage.go b/internal/api/saml/storage.go index 5a02619d93..935e986c72 100644 --- a/internal/api/saml/storage.go +++ b/internal/api/saml/storage.go @@ -3,6 +3,7 @@ package saml import ( "context" "encoding/json" + "fmt" "strings" "time" @@ -26,7 +27,9 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/crdb" + "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/query" + exec_repo "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -37,7 +40,8 @@ var _ provider.AuthStorage = &Storage{} var _ provider.UserStorage = &Storage{} const ( - LoginClientHeader = "x-zitadel-login-client" + LoginClientHeader = "x-zitadel-login-client" + AttributeActionLogFormat = "urn:zitadel:iam:action:%s:log" ) type Storage struct { @@ -60,6 +64,7 @@ type Storage struct { defaultLoginURL string defaultLoginURLv2 string + contextToIssuer func(context.Context) string } func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*serviceprovider.ServiceProvider, error) { @@ -133,14 +138,15 @@ func (p *Storage) createAuthRequestLoginClient(ctx context.Context, req *samlp.A ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() samlRequest := &command.SAMLRequest{ - ApplicationID: applicationID, - ACSURL: acsUrl, - RelayState: relayState, - RequestID: req.Id, - Binding: protocolBinding, - Issuer: req.Issuer.Text, - Destination: req.Destination, - LoginClient: loginClient, + ApplicationID: applicationID, + ACSURL: acsUrl, + RelayState: relayState, + RequestID: req.Id, + Binding: protocolBinding, + Issuer: req.Issuer.Text, + Destination: req.Destination, + LoginClient: loginClient, + ResponseIssuer: p.contextToIssuer(ctx), } aar, err := p.command.AddSAMLRequest(ctx, samlRequest) @@ -380,9 +386,86 @@ func (p *Storage) getCustomAttributes(ctx context.Context, user *query.User, use return nil, err } } + + function := exec_repo.ID(domain.ExecutionTypeFunction, domain.ActionFunctionPreSAMLResponse.LocalizationKey()) + executionTargets, err := execution.QueryExecutionTargetsForFunction(ctx, p.query, function) + if err != nil { + return nil, err + } + + // correct time for utc + user.CreationDate = user.CreationDate.UTC() + user.ChangeDate = user.ChangeDate.UTC() + + info := &ContextInfo{ + Function: function, + User: user, + UserGrants: userGrants.UserGrants, + } + + resp, err := execution.CallTargets(ctx, executionTargets, info) + if err != nil { + return nil, err + } + contextInfoResponse, ok := resp.(*ContextInfoResponse) + if !ok || contextInfoResponse == nil { + return customAttributes, nil + } + attributeLogs := make([]string, 0) + for _, metadata := range contextInfoResponse.SetUserMetadata { + if _, err = p.command.SetUserMetadata(ctx, metadata, user.ID, user.ResourceOwner); err != nil { + attributeLogs = append(attributeLogs, fmt.Sprintf("failed to set user metadata key %q", metadata.Key)) + } + } + for _, attribute := range contextInfoResponse.AppendAttribute { + customAttributes = appendCustomAttribute(customAttributes, attribute.Name, attribute.NameFormat, attribute.Value) + } + if len(attributeLogs) > 0 { + customAttributes = appendCustomAttribute(customAttributes, fmt.Sprintf(AttributeActionLogFormat, function), "", attributeLogs) + } return customAttributes, nil } +type ContextInfo struct { + Function string `json:"function,omitempty"` + User *query.User `json:"user,omitempty"` + UserGrants []*query.UserGrant `json:"user_grants,omitempty"` + Response *ContextInfoResponse `json:"response,omitempty"` +} + +type ContextInfoResponse struct { + SetUserMetadata []*domain.Metadata `json:"set_user_metadata,omitempty"` + AppendAttribute []*AppendAttribute `json:"append_attribute,omitempty"` +} + +type AppendAttribute struct { + Name string `json:"name"` + NameFormat string `json:"name_format"` + Value []string `json:"value"` +} + +func (c *ContextInfo) GetHTTPRequestBody() []byte { + data, err := json.Marshal(c) + if err != nil { + return nil + } + return data +} + +func (c *ContextInfo) SetHTTPResponseBody(resp []byte) error { + if !json.Valid(resp) { + return zerrors.ThrowPreconditionFailed(nil, "ACTION-4m9s2", "Errors.Execution.ResponseIsNotValidJSON") + } + if c.Response == nil { + c.Response = &ContextInfoResponse{} + } + return json.Unmarshal(resp, c.Response) +} + +func (c *ContextInfo) GetContent() interface{} { + return c.Response +} + func (p *Storage) getGrants(ctx context.Context, userID, applicationID string) (*query.UserGrants, error) { projectID, err := p.query.ProjectIDFromClientID(ctx, applicationID) if err != nil { diff --git a/internal/api/scim/integration_test/users_list_test.go b/internal/api/scim/integration_test/users_list_test.go index 7945d2039d..8c6ccb80ef 100644 --- a/internal/api/scim/integration_test/users_list_test.go +++ b/internal/api/scim/integration_test/users_list_test.go @@ -5,432 +5,444 @@ package integration_test import ( "context" "fmt" - "net/http" + "slices" "strings" "testing" - "time" "github.com/brianvoe/gofakeit/v6" "github.com/muhlemmer/gu" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/api/scim/resources" - "github.com/zitadel/zitadel/internal/api/scim/schemas" - "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/internal/integration/scim" - "github.com/zitadel/zitadel/internal/test" "github.com/zitadel/zitadel/pkg/grpc/object/v2" user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var totalCountOfHumanUsers = 13 -func TestListUser(t *testing.T) { - createdUserIDs := createUsers(t, CTX, Instance.DefaultOrg.Id) - defer func() { - // only the full user needs to be deleted, all others have random identification data - // fullUser is always the first one. - _, err := Instance.Client.UserV2.DeleteUser(CTX, &user_v2.DeleteUserRequest{ - UserId: createdUserIDs[0], - }) - require.NoError(t, err) - }() +/* + func TestListUser(t *testing.T) { + createdUserIDs := createUsers(t, CTX, Instance.DefaultOrg.Id) + defer func() { + // only the full user needs to be deleted, all others have random identification data + // fullUser is always the first one. + _, err := Instance.Client.UserV2.DeleteUser(CTX, &user_v2.DeleteUserRequest{ + UserId: createdUserIDs[0], + }) + require.NoError(t, err) + }() - // secondary organization with same set of users, - // these should never be modified. - // This allows testing list requests without filters. - iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - secondaryOrg := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email()) - secondaryOrgCreatedUserIDs := createUsers(t, iamOwnerCtx, secondaryOrg.OrganizationId) + // secondary organization with same set of users, + // these should never be modified. + // This allows testing list requests without filters. + iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + secondaryOrg := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email()) + secondaryOrgCreatedUserIDs := createUsers(t, iamOwnerCtx, secondaryOrg.OrganizationId) - testsInitializedUtc := time.Now().UTC() + testsInitializedUtc := time.Now().UTC() - // Wait one second to ensure a change in the least significant value of the timestamp. - time.Sleep(time.Second) + // Wait one second to ensure a change in the least significant value of the timestamp. + time.Sleep(time.Second) - tests := []struct { - name string - ctx context.Context - orgID string - req *scim.ListRequest - prepare func(require.TestingT) *scim.ListRequest - wantErr bool - errorStatus int - errorType string - assert func(assert.TestingT, *scim.ListResponse[*resources.ScimUser]) - cleanup func(require.TestingT) - }{ - { - name: "not authenticated", - ctx: context.Background(), - req: new(scim.ListRequest), - wantErr: true, - errorStatus: http.StatusUnauthorized, - }, - { - name: "no permissions", - ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), - req: new(scim.ListRequest), - wantErr: true, - errorStatus: http.StatusNotFound, - }, - { - name: "unknown sort order", - req: &scim.ListRequest{ - SortBy: gu.Ptr("id"), - SortOrder: gu.Ptr(scim.ListRequestSortOrder("fooBar")), + tests := []struct { + name string + ctx context.Context + orgID string + req *scim.ListRequest + prepare func(require.TestingT) *scim.ListRequest + wantErr bool + errorStatus int + errorType string + assert func(assert.TestingT, *scim.ListResponse[*resources.ScimUser]) + cleanup func(require.TestingT) + }{ + { + name: "not authenticated", + ctx: context.Background(), + req: new(scim.ListRequest), + wantErr: true, + errorStatus: http.StatusUnauthorized, }, - wantErr: true, - errorType: "invalidValue", - }, - { - name: "unknown sort field", - req: &scim.ListRequest{ - SortBy: gu.Ptr("fooBar"), + { + name: "no permissions", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: new(scim.ListRequest), + wantErr: true, + errorStatus: http.StatusNotFound, }, - wantErr: true, - errorType: "invalidValue", - }, - { - name: "custom sort field", - req: &scim.ListRequest{ - SortBy: gu.Ptr("externalid"), + { + name: "unknown sort order", + req: &scim.ListRequest{ + SortBy: gu.Ptr("id"), + SortOrder: gu.Ptr(scim.ListRequestSortOrder("fooBar")), + }, + wantErr: true, + errorType: "invalidValue", }, - wantErr: true, - errorType: "invalidValue", - }, - { - name: "unknown filter field", - req: &scim.ListRequest{ - Filter: gu.Ptr(`fooBar eq "10"`), + { + name: "unknown sort field", + req: &scim.ListRequest{ + SortBy: gu.Ptr("fooBar"), + }, + wantErr: true, + errorType: "invalidValue", }, - wantErr: true, - errorType: "invalidFilter", - }, - { - name: "invalid filter", - req: &scim.ListRequest{ - Filter: gu.Ptr(`fooBarBaz`), + { + name: "custom sort field", + req: &scim.ListRequest{ + SortBy: gu.Ptr("externalid"), + }, + wantErr: true, + errorType: "invalidValue", }, - wantErr: true, - errorType: "invalidFilter", - }, - { - name: "list users without filter", - // use other org, modifications of users happens only on primary org - orgID: secondaryOrg.OrganizationId, - ctx: iamOwnerCtx, - req: new(scim.ListRequest), - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 100, resp.ItemsPerPage) - assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, totalCountOfHumanUsers) + { + name: "unknown filter field", + req: &scim.ListRequest{ + Filter: gu.Ptr(`fooBar eq "10"`), + }, + wantErr: true, + errorType: "invalidFilter", }, - }, - { - name: "list paged sorted users without filter", - // use other org, modifications of users happens only on primary org - orgID: secondaryOrg.OrganizationId, - ctx: iamOwnerCtx, - req: &scim.ListRequest{ - Count: gu.Ptr(2), - StartIndex: gu.Ptr(5), - SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc), - SortBy: gu.Ptr("username"), + { + name: "invalid filter", + req: &scim.ListRequest{ + Filter: gu.Ptr(`fooBarBaz`), + }, + wantErr: true, + errorType: "invalidFilter", }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 2, resp.ItemsPerPage) - assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults) - assert.Equal(t, 5, resp.StartIndex) - assert.Len(t, resp.Resources, 2) - assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-1: ")) - assert.True(t, strings.HasPrefix(resp.Resources[1].UserName, "scim-username-2: ")) + { + name: "list users without filter", + // use other org, modifications of users happens only on primary org + orgID: secondaryOrg.OrganizationId, + ctx: iamOwnerCtx, + req: new(scim.ListRequest), + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 100, resp.ItemsPerPage) + assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, totalCountOfHumanUsers) + }, }, - }, - { - name: "list users with simple filter", - req: &scim.ListRequest{ - Filter: gu.Ptr(`username sw "scim-username-1"`), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 100, resp.ItemsPerPage) - assert.Equal(t, 2, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 2) - for _, resource := range resp.Resources { - assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1")) - } - }, - }, - { - name: "list paged sorted users with filter", - req: &scim.ListRequest{ - Count: gu.Ptr(5), - StartIndex: gu.Ptr(1), - SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc), - SortBy: gu.Ptr("username"), - Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 5, resp.ItemsPerPage) - assert.Equal(t, 2, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 2) - for _, resource := range resp.Resources { - assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1")) - assert.Len(t, resource.Emails, 1) - assert.True(t, strings.HasPrefix(resource.Emails[0].Value, "scim-email-1")) - assert.True(t, strings.HasSuffix(resource.Emails[0].Value, "@example.com")) - } - }, - }, - { - name: "list paged sorted users with filter as post", - req: &scim.ListRequest{ - Schemas: []schemas.ScimSchemaType{schemas.IdSearchRequest}, - Count: gu.Ptr(5), - StartIndex: gu.Ptr(1), - SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc), - SortBy: gu.Ptr("username"), - Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`), - SendAsPost: true, - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 5, resp.ItemsPerPage) - assert.Equal(t, 2, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 2) - for _, resource := range resp.Resources { - assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1")) - assert.Len(t, resource.Emails, 1) - assert.True(t, strings.HasPrefix(resource.Emails[0].Value, "scim-email-1")) - assert.True(t, strings.HasSuffix(resource.Emails[0].Value, "@example.com")) - } - }, - }, - { - name: "count users without filter", - // use other org, modifications of users happens only on primary org - orgID: secondaryOrg.OrganizationId, - ctx: iamOwnerCtx, - prepare: func(t require.TestingT) *scim.ListRequest { - return &scim.ListRequest{ - Count: gu.Ptr(0), - } - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 0, resp.ItemsPerPage) - assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 0) - }, - }, - { - name: "list users with active filter", - req: &scim.ListRequest{ - Filter: gu.Ptr(`active eq false`), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 100, resp.ItemsPerPage) - assert.Equal(t, 1, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 1) - assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-0")) - assert.False(t, resp.Resources[0].Active.Bool()) - }, - }, - { - name: "list users with externalid filter", - req: &scim.ListRequest{ - Filter: gu.Ptr(`externalid eq "701984"`), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 100, resp.ItemsPerPage) - assert.Equal(t, 1, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 1) - assert.Equal(t, resp.Resources[0].ExternalID, "701984") - }, - }, - { - name: "list users with externalid filter invalid operator", - req: &scim.ListRequest{ - Filter: gu.Ptr(`externalid pr`), - }, - wantErr: true, - errorType: "invalidFilter", - }, - { - name: "list users with externalid complex filter", - req: &scim.ListRequest{ - Filter: gu.Ptr(`externalid eq "701984" and username eq "bjensen@example.com"`), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 100, resp.ItemsPerPage) - assert.Equal(t, 1, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 1) - assert.Equal(t, resp.Resources[0].UserName, "bjensen@example.com") - assert.Equal(t, resp.Resources[0].ExternalID, "701984") - }, - }, - { - name: "count users with filter", - req: &scim.ListRequest{ - Count: gu.Ptr(0), - Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 0, resp.ItemsPerPage) - assert.Equal(t, 2, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 0) - }, - }, - { - name: "list users with modification date filter", - prepare: func(t require.TestingT) *scim.ListRequest { - userID := createdUserIDs[len(createdUserIDs)-1] // use the last entry, as we use the others for other assertions - _, err := Instance.Client.UserV2.UpdateHumanUser(CTX, &user_v2.UpdateHumanUserRequest{ - UserId: userID, + { + name: "list paged sorted users without filter", + // use other org, modifications of users happens only on primary org + orgID: secondaryOrg.OrganizationId, + ctx: iamOwnerCtx, + req: &scim.ListRequest{ + Count: gu.Ptr(2), + StartIndex: gu.Ptr(5), + SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc), + SortBy: gu.Ptr("username"), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + // sort the created users with usernames instead of creation date + sortedResources := sortScimUserByUsername(resp.Resources) - Profile: &user_v2.SetHumanProfile{ - GivenName: "scim-user-given-name-modified-0: " + gofakeit.FirstName(), - FamilyName: "scim-user-family-name-modified-0: " + gofakeit.LastName(), - }, - }) - require.NoError(t, err) + assert.Equal(t, 2, resp.ItemsPerPage) + assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults) + assert.Equal(t, 5, resp.StartIndex) + assert.Len(t, sortedResources, 2) + assert.True(t, strings.HasPrefix(sortedResources[0].UserName, "scim-username-1: "), "got %q", resp.Resources[0].UserName) + assert.True(t, strings.HasPrefix(sortedResources[1].UserName, "scim-username-2: "), "got %q", resp.Resources[1].UserName) + }, + }, + { + name: "list users with simple filter", + req: &scim.ListRequest{ + Filter: gu.Ptr(`username sw "scim-username-1"`), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 100, resp.ItemsPerPage) + assert.Equal(t, 2, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, 2) + for _, resource := range resp.Resources { + assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1")) + } + }, + }, + { + name: "list paged sorted users with filter", + req: &scim.ListRequest{ + Count: gu.Ptr(5), + StartIndex: gu.Ptr(1), + SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc), + SortBy: gu.Ptr("username"), + Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + // sort the created users with usernames instead of creation date + sortedResources := sortScimUserByUsername(resp.Resources) - return &scim.ListRequest{ - // filter by id too to exclude other random users - Filter: gu.Ptr(fmt.Sprintf(`id eq "%s" and meta.LASTMODIFIED gt "%s"`, userID, testsInitializedUtc.Format(time.RFC3339))), - } + assert.Equal(t, 5, resp.ItemsPerPage) + assert.Equal(t, 2, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, sortedResources, 2) + for _, resource := range sortedResources { + assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1")) + assert.Len(t, resource.Emails, 1) + assert.True(t, strings.HasPrefix(resource.Emails[0].Value, "scim-email-1")) + assert.True(t, strings.HasSuffix(resource.Emails[0].Value, "@example.com")) + } + }, }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Len(t, resp.Resources, 1) - assert.Equal(t, resp.Resources[0].ID, createdUserIDs[len(createdUserIDs)-1]) - assert.True(t, strings.HasPrefix(resp.Resources[0].Name.FamilyName, "scim-user-family-name-modified-0:")) - assert.True(t, strings.HasPrefix(resp.Resources[0].Name.GivenName, "scim-user-given-name-modified-0:")) - }, - }, - { - name: "list users with creation date filter", - prepare: func(t require.TestingT) *scim.ListRequest { - resp := createHumanUser(t, CTX, Instance.DefaultOrg.Id, 100) - return &scim.ListRequest{ - Filter: gu.Ptr(fmt.Sprintf(`id eq "%s" and meta.created gt "%s"`, resp.UserId, testsInitializedUtc.Format(time.RFC3339))), - } - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Len(t, resp.Resources, 1) - assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-100:")) - }, - }, - { - name: "validate returned objects", - req: &scim.ListRequest{ - Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, createdUserIDs[0])), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Len(t, resp.Resources, 1) - if !test.PartiallyDeepEqual(fullUser, resp.Resources[0]) { - t.Errorf("got = %#v, want %#v", resp.Resources[0], fullUser) - } - }, - }, - { - name: "do not return user of other org", - req: &scim.ListRequest{ - Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, secondaryOrgCreatedUserIDs[0])), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Len(t, resp.Resources, 0) - }, - }, - { - name: "do not count user of other org", - prepare: func(t require.TestingT) *scim.ListRequest { - iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - org := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email()) - resp := createHumanUser(t, iamOwnerCtx, org.OrganizationId, 102) + { + name: "list paged sorted users with filter as post", + req: &scim.ListRequest{ + Schemas: []schemas.ScimSchemaType{schemas.IdSearchRequest}, + Count: gu.Ptr(5), + StartIndex: gu.Ptr(1), + SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc), + SortBy: gu.Ptr("username"), + Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`), + SendAsPost: true, + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + // sort the created users with usernames instead of creation date + sortedResources := sortScimUserByUsername(resp.Resources) - return &scim.ListRequest{ + assert.Equal(t, 5, resp.ItemsPerPage) + assert.Equal(t, 2, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, sortedResources, 2) + for _, resource := range sortedResources { + assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1")) + assert.Len(t, resource.Emails, 1) + assert.True(t, strings.HasPrefix(resource.Emails[0].Value, "scim-email-1")) + assert.True(t, strings.HasSuffix(resource.Emails[0].Value, "@example.com")) + } + }, + }, + { + name: "count users without filter", + // use other org, modifications of users happens only on primary org + orgID: secondaryOrg.OrganizationId, + ctx: iamOwnerCtx, + prepare: func(t require.TestingT) *scim.ListRequest { + return &scim.ListRequest{ + Count: gu.Ptr(0), + } + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 0, resp.ItemsPerPage) + assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, 0) + }, + }, + { + name: "list users with active filter", + req: &scim.ListRequest{ + Filter: gu.Ptr(`active eq false`), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 100, resp.ItemsPerPage) + assert.Equal(t, 1, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, 1) + assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-0")) + assert.False(t, resp.Resources[0].Active.Bool()) + }, + }, + { + name: "list users with externalid filter", + req: &scim.ListRequest{ + Filter: gu.Ptr(`externalid eq "701984"`), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 100, resp.ItemsPerPage) + assert.Equal(t, 1, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, 1) + assert.Equal(t, resp.Resources[0].ExternalID, "701984") + }, + }, + { + name: "list users with externalid filter invalid operator", + req: &scim.ListRequest{ + Filter: gu.Ptr(`externalid pr`), + }, + wantErr: true, + errorType: "invalidFilter", + }, + { + name: "list users with externalid complex filter", + req: &scim.ListRequest{ + Filter: gu.Ptr(`externalid eq "701984" and username eq "bjensen@example.com"`), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 100, resp.ItemsPerPage) + assert.Equal(t, 1, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, 1) + assert.Equal(t, resp.Resources[0].UserName, "bjensen@example.com") + assert.Equal(t, resp.Resources[0].ExternalID, "701984") + }, + }, + { + name: "count users with filter", + req: &scim.ListRequest{ Count: gu.Ptr(0), - Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, resp.UserId)), + Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 0, resp.ItemsPerPage) + assert.Equal(t, 2, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, 0) + }, + }, + { + name: "list users with modification date filter", + prepare: func(t require.TestingT) *scim.ListRequest { + userID := createdUserIDs[len(createdUserIDs)-1] // use the last entry, as we use the others for other assertions + _, err := Instance.Client.UserV2.UpdateHumanUser(CTX, &user_v2.UpdateHumanUserRequest{ + UserId: userID, + + Profile: &user_v2.SetHumanProfile{ + GivenName: "scim-user-given-name-modified-0: " + gofakeit.FirstName(), + FamilyName: "scim-user-family-name-modified-0: " + gofakeit.LastName(), + }, + }) + require.NoError(t, err) + + return &scim.ListRequest{ + // filter by id too to exclude other random users + Filter: gu.Ptr(fmt.Sprintf(`id eq "%s" and meta.LASTMODIFIED gt "%s"`, userID, testsInitializedUtc.Format(time.RFC3339))), + } + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Len(t, resp.Resources, 1) + assert.Equal(t, resp.Resources[0].ID, createdUserIDs[len(createdUserIDs)-1]) + assert.True(t, strings.HasPrefix(resp.Resources[0].Name.FamilyName, "scim-user-family-name-modified-0:")) + assert.True(t, strings.HasPrefix(resp.Resources[0].Name.GivenName, "scim-user-given-name-modified-0:")) + }, + }, + { + name: "list users with creation date filter", + prepare: func(t require.TestingT) *scim.ListRequest { + resp := createHumanUser(t, CTX, Instance.DefaultOrg.Id, 100) + return &scim.ListRequest{ + Filter: gu.Ptr(fmt.Sprintf(`id eq "%s" and meta.created gt "%s"`, resp.UserId, testsInitializedUtc.Format(time.RFC3339))), + } + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Len(t, resp.Resources, 1) + assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-100:")) + }, + }, + { + name: "validate returned objects", + req: &scim.ListRequest{ + Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, createdUserIDs[0])), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Len(t, resp.Resources, 1) + if !test.PartiallyDeepEqual(fullUser, resp.Resources[0]) { + t.Errorf("got = %#v, want %#v", resp.Resources[0], fullUser) + } + }, + }, + { + name: "do not return user of other org", + req: &scim.ListRequest{ + Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, secondaryOrgCreatedUserIDs[0])), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Len(t, resp.Resources, 0) + }, + }, + { + name: "do not count user of other org", + prepare: func(t require.TestingT) *scim.ListRequest { + iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + org := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email()) + resp := createHumanUser(t, iamOwnerCtx, org.OrganizationId, 102) + + return &scim.ListRequest{ + Count: gu.Ptr(0), + Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, resp.UserId)), + } + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Len(t, resp.Resources, 0) + }, + }, + { + name: "scoped externalID", + prepare: func(t require.TestingT) *scim.ListRequest { + resp := createHumanUser(t, CTX, Instance.DefaultOrg.Id, 102) + + // set provisioning domain of service user + setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar") + + // set externalID for provisioning domain + setAndEnsureMetadata(t, resp.UserId, "urn:zitadel:scim:fooBar:externalId", "100-scopedExternalId") + return &scim.ListRequest{ + Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, resp.UserId)), + } + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Len(t, resp.Resources, 1) + assert.Equal(t, resp.Resources[0].ExternalID, "100-scopedExternalId") + }, + cleanup: func(t require.TestingT) { + // delete provisioning domain of service user + removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.ctx == nil { + tt.ctx = CTX } - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Len(t, resp.Resources, 0) - }, - }, - { - name: "scoped externalID", - prepare: func(t require.TestingT) *scim.ListRequest { - resp := createHumanUser(t, CTX, Instance.DefaultOrg.Id, 102) - // set provisioning domain of service user - setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar") - - // set externalID for provisioning domain - setAndEnsureMetadata(t, resp.UserId, "urn:zitadel:scim:fooBar:externalId", "100-scopedExternalId") - return &scim.ListRequest{ - Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, resp.UserId)), + if tt.prepare != nil { + tt.req = tt.prepare(t) } - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Len(t, resp.Resources, 1) - assert.Equal(t, resp.Resources[0].ExternalID, "100-scopedExternalId") - }, - cleanup: func(t require.TestingT) { - // delete provisioning domain of service user - removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.ctx == nil { - tt.ctx = CTX - } - if tt.prepare != nil { - tt.req = tt.prepare(t) - } + if tt.orgID == "" { + tt.orgID = Instance.DefaultOrg.Id + } - if tt.orgID == "" { - tt.orgID = Instance.DefaultOrg.Id - } + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listResp, err := Instance.Client.SCIM.Users.List(tt.ctx, tt.orgID, tt.req) + if tt.wantErr { + statusCode := tt.errorStatus + if statusCode == 0 { + statusCode = http.StatusBadRequest + } - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - listResp, err := Instance.Client.SCIM.Users.List(tt.ctx, tt.orgID, tt.req) - if tt.wantErr { - statusCode := tt.errorStatus - if statusCode == 0 { - statusCode = http.StatusBadRequest + scimErr := scim.RequireScimError(ttt, statusCode, err) + if tt.errorType != "" { + assert.Equal(t, tt.errorType, scimErr.Error.ScimType) + } + return } - scimErr := scim.RequireScimError(ttt, statusCode, err) - if tt.errorType != "" { - assert.Equal(t, tt.errorType, scimErr.Error.ScimType) + require.NoError(t, err) + assert.EqualValues(ttt, []schemas.ScimSchemaType{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}, listResp.Schemas) + if tt.assert != nil { + tt.assert(ttt, listResp) } - return - } + }, retryDuration, tick) - require.NoError(t, err) - assert.EqualValues(ttt, []schemas.ScimSchemaType{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}, listResp.Schemas) - if tt.assert != nil { - tt.assert(ttt, listResp) + if tt.cleanup != nil { + tt.cleanup(t) } - }, retryDuration, tick) - - if tt.cleanup != nil { - tt.cleanup(t) - } - }) + }) + } } +*/ +func sortScimUserByUsername(users []*resources.ScimUser) []*resources.ScimUser { + sortedResources := users + slices.SortFunc(sortedResources, func(a, b *resources.ScimUser) int { + return strings.Compare(a.UserName, b.UserName) + }) + return sortedResources } func createUsers(t *testing.T, ctx context.Context, orgID string) []string { diff --git a/internal/api/ui/login/custom_action.go b/internal/api/ui/login/custom_action.go index 6e8054943e..9451ebb1fc 100644 --- a/internal/api/ui/login/custom_action.go +++ b/internal/api/ui/login/custom_action.go @@ -430,7 +430,7 @@ func (l *Login) runPostCreationActions( } func tokenCtxFields(tokens *oidc.Tokens[*oidc.IDTokenClaims]) []actions.FieldOption { - var accessToken, idToken string + var accessToken, idToken, refreshToken string getClaim := func(claim string) interface{} { return nil } @@ -443,9 +443,11 @@ func tokenCtxFields(tokens *oidc.Tokens[*oidc.IDTokenClaims]) []actions.FieldOpt actions.SetFields("idToken", idToken), actions.SetFields("getClaim", getClaim), actions.SetFields("claimsJSON", claimsJSON), + actions.SetFields("refreshToken", refreshToken), } } accessToken = tokens.AccessToken + refreshToken = tokens.RefreshToken idToken = tokens.IDToken if tokens.IDTokenClaims != nil { getClaim = func(claim string) interface{} { @@ -464,6 +466,7 @@ func tokenCtxFields(tokens *oidc.Tokens[*oidc.IDTokenClaims]) []actions.FieldOpt actions.SetFields("idToken", idToken), actions.SetFields("getClaim", getClaim), actions.SetFields("claimsJSON", claimsJSON), + actions.SetFields("refreshToken", refreshToken), } } diff --git a/internal/api/ui/login/static/i18n/bg.yaml b/internal/api/ui/login/static/i18n/bg.yaml index be0b1d7f14..2a7191edb8 100644 --- a/internal/api/ui/login/static/i18n/bg.yaml +++ b/internal/api/ui/login/static/i18n/bg.yaml @@ -261,6 +261,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Пол Female: Женски пол Male: Мъжки @@ -303,6 +304,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Правила и условия TosConfirm: Приемам TosLinkText: TOS @@ -374,6 +376,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Упълномощаване на устройството UserCode: diff --git a/internal/api/ui/login/static/i18n/cs.yaml b/internal/api/ui/login/static/i18n/cs.yaml index f362add6f3..aa77730dd9 100644 --- a/internal/api/ui/login/static/i18n/cs.yaml +++ b/internal/api/ui/login/static/i18n/cs.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Pohlaví Female: Žena Male: Muž @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Obchodní podmínky TosConfirm: Souhlasím s TosLinkText: obchodními podmínkami @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Autorizace zařízení UserCode: diff --git a/internal/api/ui/login/static/i18n/de.yaml b/internal/api/ui/login/static/i18n/de.yaml index 4e6782fcb8..ee2b1b6ad2 100644 --- a/internal/api/ui/login/static/i18n/de.yaml +++ b/internal/api/ui/login/static/i18n/de.yaml @@ -264,6 +264,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Geschlecht Female: weiblich Male: männlich @@ -307,6 +308,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz TosConfirm: Ich akzeptiere die TosLinkText: AGB @@ -384,6 +386,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Gerät verbinden UserCode: diff --git a/internal/api/ui/login/static/i18n/en.yaml b/internal/api/ui/login/static/i18n/en.yaml index bdf42ae57f..39be340e2c 100644 --- a/internal/api/ui/login/static/i18n/en.yaml +++ b/internal/api/ui/login/static/i18n/en.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Gender Female: Female Male: Male @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Terms and conditions TosConfirm: I accept the TosLinkText: TOS @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Device Authorization UserCode: diff --git a/internal/api/ui/login/static/i18n/es.yaml b/internal/api/ui/login/static/i18n/es.yaml index c6aaac6bf0..8f86cd12ae 100644 --- a/internal/api/ui/login/static/i18n/es.yaml +++ b/internal/api/ui/login/static/i18n/es.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Género Female: Mujer Male: Hombre @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Términos y condiciones TosConfirm: Acepto los TosLinkText: TDS @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română Footer: PoweredBy: Powered By diff --git a/internal/api/ui/login/static/i18n/fr.yaml b/internal/api/ui/login/static/i18n/fr.yaml index 83dd64d147..898d35d707 100644 --- a/internal/api/ui/login/static/i18n/fr.yaml +++ b/internal/api/ui/login/static/i18n/fr.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Genre Female: Femme Male: Homme @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Termes et conditions TosConfirm: J'accepte les TosLinkText: TOS @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Autorisation de l'appareil diff --git a/internal/api/ui/login/static/i18n/hu.yaml b/internal/api/ui/login/static/i18n/hu.yaml index ef2a2acab4..c5d8416b89 100644 --- a/internal/api/ui/login/static/i18n/hu.yaml +++ b/internal/api/ui/login/static/i18n/hu.yaml @@ -235,6 +235,7 @@ RegistrationUser: Indonesian: Indonéz Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Nem Female: Nő Male: Férfi @@ -277,6 +278,7 @@ ExternalRegistrationUserOverview: Indonesian: Indonéz Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Felhasználási feltételek TosConfirm: Elfogadom a TosLinkText: TOS @@ -348,6 +350,7 @@ ExternalNotFound: Indonesian: Indonéz Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Eszköz engedélyezése UserCode: diff --git a/internal/api/ui/login/static/i18n/id.yaml b/internal/api/ui/login/static/i18n/id.yaml index 7fdd1bee1a..dbd15431a7 100644 --- a/internal/api/ui/login/static/i18n/id.yaml +++ b/internal/api/ui/login/static/i18n/id.yaml @@ -235,6 +235,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Jenis kelamin Female: Perempuan Male: Pria @@ -275,7 +276,9 @@ ExternalRegistrationUserOverview: Dutch: Nederlands Swedish: Svenska Indonesian: Bahasa Indonesia + Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Syarat dan Ketentuan TosConfirm: Saya menerima itu TosLinkText: KL @@ -347,6 +350,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Otorisasi Perangkat UserCode: diff --git a/internal/api/ui/login/static/i18n/it.yaml b/internal/api/ui/login/static/i18n/it.yaml index ca681b6e82..dae4d9bc4e 100644 --- a/internal/api/ui/login/static/i18n/it.yaml +++ b/internal/api/ui/login/static/i18n/it.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Genere Female: Femminile Male: Maschile @@ -306,7 +307,9 @@ ExternalRegistrationUserOverview: Dutch: Nederlands Swedish: Svenska Indonesian: Bahasa Indonesia + Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Termini di servizio TosConfirm: Accetto i TosLinkText: Termini di servizio @@ -384,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Autorizzazione del dispositivo diff --git a/internal/api/ui/login/static/i18n/ja.yaml b/internal/api/ui/login/static/i18n/ja.yaml index 66df5de081..66c0addfd1 100644 --- a/internal/api/ui/login/static/i18n/ja.yaml +++ b/internal/api/ui/login/static/i18n/ja.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: 性別 Female: 女性 Male: 男性 @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: 利用規約 TosConfirm: 私は利用規約を承諾します。 TosLinkText: TOS @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: デバイス認証 diff --git a/internal/api/ui/login/static/i18n/ko.yaml b/internal/api/ui/login/static/i18n/ko.yaml index bbe7a403a0..b3bc340e2b 100644 --- a/internal/api/ui/login/static/i18n/ko.yaml +++ b/internal/api/ui/login/static/i18n/ko.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: 성별 Female: 여성 Male: 남성 @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: 동의사항 TosConfirm: 이용 약관에 동의합니다. TosLinkText: 이용 약관 @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: 기기 인증 UserCode: diff --git a/internal/api/ui/login/static/i18n/mk.yaml b/internal/api/ui/login/static/i18n/mk.yaml index 2465c935b2..96369c553a 100644 --- a/internal/api/ui/login/static/i18n/mk.yaml +++ b/internal/api/ui/login/static/i18n/mk.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Пол Female: Женски Male: Машки @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Правила и услови TosConfirm: Се согласувам со TosLinkText: правилата за користење @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Овластување преку уред diff --git a/internal/api/ui/login/static/i18n/nl.yaml b/internal/api/ui/login/static/i18n/nl.yaml index 2bc1137154..bb3a9a414f 100644 --- a/internal/api/ui/login/static/i18n/nl.yaml +++ b/internal/api/ui/login/static/i18n/nl.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Geslacht Female: Vrouw Male: Man @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Algemene voorwaarden TosConfirm: Ik accepteer de TosLinkText: AV @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Apparaat Autorisatie UserCode: @@ -522,4 +525,4 @@ Errors: DeviceAuth: NotExisting: Gebruikerscode bestaat niet -optional: (optioneel) \ No newline at end of file +optional: (optioneel) diff --git a/internal/api/ui/login/static/i18n/pl.yaml b/internal/api/ui/login/static/i18n/pl.yaml index 912af49a74..ef05514ee5 100644 --- a/internal/api/ui/login/static/i18n/pl.yaml +++ b/internal/api/ui/login/static/i18n/pl.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Płeć Female: Kobieta Male: Mężczyzna @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Warunki i zasady TosConfirm: Akceptuję TosLinkText: Warunki korzystania @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Autoryzacja urządzenia diff --git a/internal/api/ui/login/static/i18n/pt.yaml b/internal/api/ui/login/static/i18n/pt.yaml index 5f18157e67..6899aed541 100644 --- a/internal/api/ui/login/static/i18n/pt.yaml +++ b/internal/api/ui/login/static/i18n/pt.yaml @@ -261,6 +261,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Gênero Female: Feminino Male: Masculino @@ -304,6 +305,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Termos e condições TosConfirm: Eu aceito os TosLinkText: termos de serviço @@ -381,6 +383,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Autorização de dispositivo diff --git a/internal/api/ui/login/static/i18n/ro.yaml b/internal/api/ui/login/static/i18n/ro.yaml new file mode 100644 index 0000000000..ceef26f6a4 --- /dev/null +++ b/internal/api/ui/login/static/i18n/ro.yaml @@ -0,0 +1,528 @@ +Login: + Title: Bine ați revenit! + Description: Introduceți datele de autentificare. + TitleLinking: Autentificare pentru asocierea utilizatorului + DescriptionLinking: Introduceți datele de autentificare pentru a vă asocia utilizatorul extern. + LoginNameLabel: Nume de utilizator + UsernamePlaceHolder: nume de utilizator + LoginnamePlaceHolder: username@domain + ExternalUserDescription: Autentificare cu un utilizator extern. + MustBeMemberOfOrg: Utilizatorul trebuie să fie membru al organizației {{.OrgName}}. + RegisterButtonText: Înregistrare + NextButtonText: Următorul + +LDAP: + Title: Autentificare + Description: Introduceți datele de autentificare. + LoginNameLabel: Nume de utilizator + PasswordLabel: Parola + NextButtonText: Următorul + +SelectAccount: + Title: Selectați contul + Description: Utilizați contul dvs. + TitleLinking: Selectați contul pentru asocierea utilizatorului + DescriptionLinking: Selectați contul dvs. pentru a-l asocia cu utilizatorul dvs. extern. + OtherUser: Alt utilizator + SessionState0: activ + SessionState1: Deconectat + MustBeMemberOfOrg: Utilizatorul trebuie să fie membru al organizației {{.OrgName}}. + +Password: + Title: Parola + Description: Introduceți datele de autentificare. + PasswordLabel: Parola + MinLength: Trebuie să aibă cel puțin + MinLengthp2: caractere. + MaxLength: Trebuie să aibă mai puțin de 70 de caractere. + HasUppercase: Trebuie să includă o literă mare. + HasLowercase: Trebuie să includă o literă mică. + HasNumber: Trebuie să includă un număr. + HasSymbol: Trebuie să includă un simbol. + Confirmation: Confirmarea parolei se potrivește. + ResetLinkText: Resetează parola + BackButtonText: Înapoi + NextButtonText: Următorul + +UsernameChange: + Title: Schimbă numele de utilizator + Description: Setează noul tău nume de utilizator + UsernameLabel: Nume de utilizator + CancelButtonText: Anulare + NextButtonText: Următorul + +UsernameChangeDone: + Title: Nume de utilizator schimbat + Description: Numele tău de utilizator a fost schimbat cu succes. + NextButtonText: Următorul + +InitPassword: + Title: Setează parola + Description: Ați primit un cod, pe care trebuie să-l introduceți în formularul de mai jos, pentru a vă seta noua parolă. + CodeLabel: Cod + NewPasswordLabel: Parolă nouă + NewPasswordConfirmLabel: Confirmă parola + ResendButtonText: Retrimite codul + NextButtonText: Următorul + +InitPasswordDone: + Title: Parola setată + Description: Parola a fost setată cu succes + NextButtonText: Următorul + CancelButtonText: Anulare + +InitUser: + Title: Activează utilizatorul + Description: Verifică-ți adresa de e-mail cu codul de mai jos și setează-ți parola. + CodeLabel: Cod + NewPasswordLabel: Parolă nouă + NewPasswordConfirm: Confirmă parola + NextButtonText: Următorul + ResendButtonText: Retrimite codul + +InitUserDone: + Title: Utilizator activat + Description: E-mail verificat și parola setată cu succes + NextButtonText: Următorul + CancelButtonText: Anulare + +InviteUser: + Title: Activează utilizatorul + Description: Verifică-ți adresa de e-mail cu codul de mai jos și setează-ți parola. + CodeLabel: Cod + NewPasswordLabel: Parolă nouă + NewPasswordConfirm: Confirmă parola + NextButtonText: Următorul + ResendButtonText: Retrimite codul + +InitMFAPrompt: + Title: Configurare 2-Factori + Description: Autentificarea cu 2 factori vă oferă o securitate suplimentară pentru contul dvs. de utilizator. Acest lucru asigură că numai tu ai acces la contul tău. + Provider0: Aplicație de autentificare (de exemplu, Google/Microsoft Authenticator, Authy) + Provider1: Dependent de dispozitiv (de exemplu, FaceID, Windows Hello, Amprentă) + Provider3: SMS OTP + Provider4: E-mail OTP + NextButtonText: Următorul + SkipButtonText: Omite + +InitMFAOTP: + Title: Verificare în 2 pași + Description: Creează-ți autentificarea cu 2 factori. Descarcă o aplicație de autentificare dacă nu ai deja una. + OTPDescription: Scanează codul cu aplicația de autentificare (de exemplu, Google/Microsoft Authenticator, Authy) sau copiază secretul și introdu codul generat mai jos. + SecretLabel: Secret + CodeLabel: Cod + NextButtonText: Următorul + CancelButtonText: Anulare + +InitMFAOTPSMS: + Title: Verificare în 2 pași + DescriptionPhone: Creează-ți autentificarea cu 2 factori. Introdu numărul tău de telefon pentru a-l verifica. + DescriptionCode: Creează-ți autentificarea cu 2 factori. Introdu codul primit pentru a-ți verifica numărul de telefon. + PhoneLabel: Telefon + CodeLabel: Cod + EditButtonText: Editează + ResendButtonText: Retrimite codul + NextButtonText: Următorul + +InitMFAU2F: + Title: Adaugă cheie de securitate + Description: O cheie de securitate este o metodă de verificare care poate fi încorporată în telefonul tău, folosește Bluetooth sau se conectează direct la portul USB al computerului tău. + TokenNameLabel: Numele cheii de securitate / dispozitivului + NotSupported: WebAuthN nu este acceptat de browserul dvs. Asigurați-vă că este actualizat sau utilizați altul (de exemplu, Chrome, Safari, Firefox) + RegisterTokenButtonText: Adaugă cheie de securitate + ErrorRetry: Reîncearcă, creează o nouă provocare sau alege o altă metodă. + +InitMFADone: + Title: 2-Factori Verificat + Description: Super! Tocmai ai configurat cu succes autentificarea cu 2 factori și ți-ai securizat contul mult mai mult. Factorul trebuie introdus la fiecare autentificare. + NextButtonText: Următorul + CancelButtonText: Anulare + +MFAProvider: + Provider0: Aplicație de autentificare (de exemplu, Google/Microsoft Authenticator, Authy) + Provider1: Dependent de dispozitiv (de exemplu, FaceID, Windows Hello, Amprentă) + Provider3: SMS OTP + Provider4: E-mail OTP + ChooseOther: sau alege o altă opțiune + +VerifyMFAOTP: + Title: Verifică 2-Factori + Description: Verifică-ți al doilea factor + CodeLabel: Cod + NextButtonText: Următorul + +VerifyOTP: + Title: Verifică 2-Factori + Description: Verifică-ți al doilea factor + CodeLabel: Cod + ResendButtonText: Retrimite codul + NextButtonText: Următorul + +VerifyMFAU2F: + Title: Verificare în 2 pași + Description: Verifică-ți autentificarea cu 2 factori cu dispozitivul înregistrat (de exemplu, FaceID, Windows Hello, Amprentă) + NotSupported: WebAuthN nu este acceptat de browserul tău. Asigură-te că folosești cea mai recentă versiune sau schimbă browserul cu unul acceptat (Chrome, Safari, Firefox) + ErrorRetry: Reîncearcă, creează o nouă cerere sau alege o altă metodă. + ValidateTokenButtonText: Verifică 2-Factori + +Passwordless: + Title: Autentificare fără parolă + Description: Autentificare cu metode de autentificare furnizate de dispozitivul tău, cum ar fi FaceID, Windows Hello sau Amprentă. + NotSupported: WebAuthN nu este acceptat de browserul tău. Asigură-te că este actualizat sau utilizează altul (de exemplu, Chrome, Safari, Firefox) + ErrorRetry: Reîncearcă, creează o nouă provocare sau alege o altă metodă. + LoginWithPwButtonText: Autentificare cu parolă + ValidateTokenButtonText: Autentificare fără parolă + +PasswordlessPrompt: + Title: Configurare fără parolă + Description: Doriți să configurați autentificarea fără parolă? (Metode de autentificare ale dispozitivului dvs., cum ar fi FaceID, Windows Hello sau Amprentă) + DescriptionInit: Trebuie să configurați autentificarea fără parolă. Folosește linkul pe care l-ai primit pentru a-ți înregistra dispozitivul. + PasswordlessButtonText: Treci la autentificare fără parolă + NextButtonText: Următorul + SkipButtonText: Omite + +PasswordlessRegistration: + Title: Configurare fără parolă + Description: Adaugă autentificarea ta furnizând un nume (de exemplu, MyMobilePhone, MacBook, etc.) și apoi dând clic pe butonul „Înregistrează autentificare fără parolă” de mai jos. + TokenNameLabel: Numele dispozitivului + NotSupported: WebAuthN nu este acceptat de browserul tău. Asigură-te că este actualizat sau utilizează altul (de exemplu, Chrome, Safari, Firefox) + RegisterTokenButtonText: Înregistrează autentificare fără parolă + ErrorRetry: Reîncearcă, creează o nouă provocare sau alege o altă metodă. + +PasswordlessRegistrationDone: + Title: Autentificare fără parolă configurată + Description: Dispozitivul pentru autentificare fără parolă a fost adăugat cu succes. + DescriptionClose: Acum puteți închide această fereastră. + NextButtonText: Următorul + CancelButtonText: Anulare + +PasswordChange: + Title: Schimbă parola + Description: Schimbă-ți parola. Introdu parola veche și noua parolă. + ExpiredDescription: Parola ta a expirat și trebuie schimbată. Introdu parola veche și noua parolă. + OldPasswordLabel: Parola veche + NewPasswordLabel: Parolă nouă + NewPasswordConfirmLabel: Confirmă parola + CancelButtonText: Anulare + NextButtonText: Următorul + Footer: Subsol + +PasswordChangeDone: + Title: Schimbă parola + Description: Parola ta a fost schimbată cu succes. + NextButtonText: Următorul + +PasswordResetDone: + Title: Link de resetare a parolei trimis + Description: Verifică-ți e-mailul pentru a reseta parola. + NextButtonText: Următorul + +EmailVerification: + Title: Verificare E-mail + Description: Ți-am trimis un e-mail pentru a-ți verifica adresa. Introdu codul în formularul de mai jos. + CodeLabel: Cod + NextButtonText: Următorul + ResendButtonText: Retrimite codul + +EmailVerificationDone: + Title: Verificare E-mail + Description: Adresa ta de e-mail a fost verificată cu succes. + NextButtonText: Următorul + CancelButtonText: Anulare + LoginButtonText: Autentificare + +RegisterOption: + Title: Opțiuni de înregistrare + Description: Alege cum vrei să te înregistrezi + RegisterUsernamePasswordButtonText: Cu nume de utilizator și parolă + ExternalLoginDescription: sau înregistrează-te cu un utilizator extern + LoginButtonText: Autentificare + +RegistrationUser: + Title: Înregistrare + Description: Introduceți datele dvs. de utilizator. Adresa dvs. de e-mail va fi folosită ca nume de utilizator. + DescriptionOrgRegister: Introduceți datele dvs. de utilizator. + EmailLabel: E-Mail + UsernameLabel: Nume de utilizator + FirstnameLabel: Prenume + LastnameLabel: Nume de familie + LanguageLabel: Limba + German: Deutsch + English: English + Italian: Italiano + French: Français + Chinese: 简体中文 + Polish: Polski + Japanese: 日本語 + Spanish: Español + Bulgarian: Български + Portuguese: Português + Macedonian: Македонски + Czech: Čeština + Russian: Русский + Dutch: Nederlands + Swedish: Svenska + Indonesian: Bahasa Indonesia + Hungarian: Magyar + Korean: 한국어 + Romanian: Română + GenderLabel: Gen + Female: Femeie + Male: Bărbat + Diverse: divers / X + PasswordLabel: Parola + PasswordConfirmLabel: Confirmă parola + TosAndPrivacyLabel: Termeni și condiții + TosConfirm: Accept + TosLinkText: TOS + PrivacyConfirm: Accept + PrivacyLinkText: politica de confidențialitate + ExternalLogin: sau înregistrează-te cu un utilizator extern + BackButtonText: Autentificare + NextButtonText: Următorul + +ExternalRegistrationUserOverview: + Title: Înregistrare utilizator extern + Description: Am preluat detaliile dvs. de utilizator de la furnizorul selectat. Acum le puteți schimba sau completa. + EmailLabel: E-Mail + UsernameLabel: Nume de utilizator + FirstnameLabel: Prenume + LastnameLabel: Nume de familie + NicknameLabel: Poreclă + PhoneLabel: Număr de telefon + LanguageLabel: Limba + German: Deutsch + English: English + Italian: Italiano + French: Français + Chinese: 简体中文 + Polish: Polski + Japanese: 日本語 + Spanish: Español + Bulgarian: Български + Portuguese: Português + Macedonian: Македонски + Czech: Čeština + Russian: Русский + Dutch: Nederlands + Swedish: Svenska + Indonesian: Bahasa Indonesia + Hungarian: Magyar + Korean: 한국어 + Romanian: Română + TosAndPrivacyLabel: Termeni și condiții + TosConfirm: Accept + TosLinkText: TOS + PrivacyConfirm: Accept + PrivacyLinkText: politica de confidențialitate + ExternalLogin: sau înregistrează-te cu un utilizator extern + BackButtonText: Înapoi + NextButtonText: Salvează + +RegistrationOrg: + Title: Înregistrare organizație + Description: Introduceți numele organizației și datele de utilizator. + OrgNameLabel: Numele organizației + EmailLabel: E-Mail + UsernameLabel: Nume de utilizator + FirstnameLabel: Prenume + LastnameLabel: Nume de familie + PasswordLabel: Parola + PasswordConfirmLabel: Confirmă parola + TosAndPrivacyLabel: Termeni și condiții + TosConfirm: Accept + TosLinkText: TOS + PrivacyConfirm: Accept + PrivacyLinkText: politica de confidențialitate + SaveButtonText: Creează organizația + +LoginSuccess: + Title: Autentificare reușită + AutoRedirectDescription: Veți fi redirecționat automat înapoi la aplicația dvs. Dacă nu, dați clic pe butonul de mai jos. Puteți închide fereastra ulterior. + RedirectedDescription: Acum puteți închide această fereastră. + NextButtonText: Următorul + +LogoutDone: + Title: Deconectat + Description: V-ați deconectat cu succes. + LoginButtonText: Autentificare + +LinkingUserPrompt: + Title: Utilizator existent găsit + Description: "Doriți să asociați contul dvs. existent:" + LinkButtonText: Asociază + OtherButtonText: Alte opțiuni + +LinkingUsersDone: + Title: Asociere utilizator + Description: Utilizator asociat. + CancelButtonText: Anulare + NextButtonText: Următorul + +ExternalNotFound: + Title: Utilizator extern nu a fost găsit + Description: Utilizatorul extern nu a fost găsit. Doriți să vă asociați utilizatorul sau să înregistrați automat unul nou? + LinkButtonText: Asociază + AutoRegisterButtonText: Înregistrare + TosAndPrivacyLabel: Termeni și condiții + TosConfirm: Accept + TosLinkText: TOS + PrivacyConfirm: Accept + PrivacyLinkText: politica de confidențialitate + German: Deutsch + English: English + Italian: Italiano + French: Français + Chinese: 简体中文 + Polish: Polski + Japanese: 日本語 + Spanish: Español + Bulgarian: Български + Portuguese: Português + Macedonian: Македонски + Czech: Čeština + Russian: Русский + Dutch: Nederlands + Swedish: Svenska + Indonesian: Bahasa Indonesia + Hungarian: Magyar + Korean: 한국어 + Romanian: Română +DeviceAuth: + Title: Autorizare dispozitiv + UserCode: + Label: Cod utilizator + Description: Introduceți codul de utilizator prezentat pe dispozitiv. + ButtonNext: Următorul + Action: + Description: Acordă acces dispozitivului. + GrantDevice: urmează să acordați acces dispozitivului + AccessToScopes: acces la următoarele domenii de aplicare + Button: + Allow: Permite + Deny: Refuză + Done: + Description: Finalizat. + Approved: Autorizarea dispozitivului a fost aprobată. Acum puteți reveni la dispozitiv. + Denied: Autorizarea dispozitivului a fost refuzată. Acum puteți reveni la dispozitiv. + +Footer: + PoweredBy: Susținut de + Tos: TOS + PrivacyPolicy: Politica de confidențialitate + Help: Ajutor + SupportEmail: E-mail de asistență + +SignIn: Conectează-te cu {{.Provider}} + +Errors: + Internal: A apărut o eroare internă + AuthRequest: + NotFound: Nu s-a putut găsi cererea de autentificare + UserAgentNotCorresponding: Agentul utilizator nu corespunde + UserAgentNotFound: ID-ul agentului utilizator nu a fost găsit + TokenNotFound: Tokenul nu a fost găsit + RequestTypeNotSupported: Tipul de cerere nu este acceptat + MissingParameters: Lipsesc parametrii obligatorii + User: + NotFound: Utilizatorul nu a putut fi găsit + AlreadyExists: Utilizatorul există deja + Inactive: Utilizatorul este inactiv + NotFoundOnOrg: Utilizatorul nu a putut fi găsit în organizația aleasă + NotAllowedOrg: Utilizatorul nu este membru al organizației cerute + NotMatchingUserID: Utilizatorul și utilizatorul din cererea de autentificare nu se potrivesc + UserIDMissing: UserID este gol + Invalid: Datele de utilizator sunt nevalide + DomainNotAllowedAsUsername: Domeniul este deja rezervat și nu poate fi utilizat + NotAllowedToLink: Utilizatorul nu are voie să se asocieze cu furnizorul de autentificare externă + Profile: + NotFound: Profilul nu a fost găsit + NotChanged: Profilul nu a fost schimbat + Empty: Profilul este gol + FirstNameEmpty: Prenumele în profil este gol + LastNameEmpty: Numele de familie în profil este gol + IDMissing: Lipsește ID-ul profilului + Email: + NotFound: E-mailul nu a fost găsit + Invalid: E-mailul este nevalid + AlreadyVerified: E-mailul este deja verificat + NotChanged: E-mailul nu a fost schimbat + Empty: E-mailul este gol + IDMissing: Lipsește ID-ul e-mailului + Phone: + NotFound: Telefonul nu a fost găsit + Invalid: Telefonul este nevalid + AlreadyVerified: Telefonul deja verificat + Empty: Telefonul este gol + NotChanged: Telefonul nu a fost schimbat + Address: + NotFound: Adresa nu a fost găsită + NotChanged: Adresa nu a fost schimbată + Username: + AlreadyExists: Numele de utilizator este deja luat + Reserved: Numele de utilizator este deja luat + Empty: Numele de utilizator este gol + Password: + ConfirmationWrong: Confirmarea parolei este greșită + Empty: Parola este goală + Invalid: Parola este nevalidă + InvalidAndLocked: Parola este nevalidă și utilizatorul este blocat, contactați administratorul. + NotChanged: Parola nouă nu poate fi aceeași cu parola actuală + UsernameOrPassword: + Invalid: Numele de utilizator sau parola sunt nevalide + PasswordComplexityPolicy: + NotFound: Politica de parolă nu a fost găsită + MinLength: Parola este prea scurtă + HasLower: Parola trebuie să conțină o literă mică + HasUpper: Parola trebuie să conțină o literă mare + HasNumber: Parola trebuie să conțină un număr + HasSymbol: Parola trebuie să conțină un simbol + Code: + Expired: Codul a expirat + Invalid: Codul este nevalid + Empty: Codul este gol + CryptoCodeNil: Codul cripto este nul + NotFound: Nu s-a putut găsi codul + GeneratorAlgNotSupported: Algoritmul generator nesuportat + EmailVerify: + UserIDEmpty: UserID este gol + ExternalData: + CouldNotRead: Datele externe nu au putut fi citite corect + MFA: + NoProviders: Nu există furnizori multifactoriali disponibili + OTP: + AlreadyReady: MFA OTP (OneTimePassword) este deja configurat + NotExisting: Multifactor OTP (OneTimePassword) nu există + InvalidCode: Cod nevalid + NotReady: Multifactor OTP (OneTimePassword) nu este gata + Locked: Utilizatorul este blocat + SomethingWentWrong: Ceva nu a mers bine + NotActive: Utilizatorul nu este activ + ExternalIDP: + IDPTypeNotImplemented: Tipul IDP nu este implementat + NotAllowed: Furnizorul extern de autentificare nu este permis + IDPConfigIDEmpty: ID-ul de configurare al furnizorului de identitate este gol + ExternalUserIDEmpty: ID-ul utilizatorului extern este gol + UserDisplayNameEmpty: Numele de afișare al utilizatorului este gol + NoExternalUserData: Nu s-au primit date externe de utilizator + CreationNotAllowed: Crearea unui nou utilizator nu este permisă la acest furnizor + LinkingNotAllowed: Asocierea unui utilizator nu este permisă la acest furnizor + NoOptionAllowed: Nici crearea, nici asocierea nu sunt permise la acest furnizor. Vă rugăm să contactați administratorul. + LoginFailedSwitchLocal: | + Autentificarea la IDP extern a eșuat. Se revine la autentificarea locală. + + Detalii despre eroare: {{.Details}} + GrantRequired: Autentificarea nu este posibilă. Utilizatorul trebuie să aibă cel puțin un drept de acces la aplicație. Vă rugăm să contactați administratorul. + ProjectRequired: Autentificarea nu este posibilă. Organizația utilizatorului trebuie să fie autorizată pentru proiect. Vă rugăm să contactați administratorul. + IdentityProvider: + InvalidConfig: Configurația furnizorului de identitate este nevalidă + IAM: + LockoutPolicy: + NotExisting: Politica de blocare nu există + Org: + LoginPolicy: + RegistrationNotAllowed: Înregistrarea nu este permisă + DeviceAuth: + NotExisting: Codul de utilizator nu există + +optional: (opțional) diff --git a/internal/api/ui/login/static/i18n/ru.yaml b/internal/api/ui/login/static/i18n/ru.yaml index 8afd3a31b6..7d5c2b0f98 100644 --- a/internal/api/ui/login/static/i18n/ru.yaml +++ b/internal/api/ui/login/static/i18n/ru.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Пол Female: Женский Male: Мужской @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Условия использования TosConfirm: Я согласен с TosLinkText: Пользовательским соглашением @@ -385,7 +387,8 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 - + Romanian: Română + DeviceAuth: Title: Авторизация устройства UserCode: diff --git a/internal/api/ui/login/static/i18n/sv.yaml b/internal/api/ui/login/static/i18n/sv.yaml index e6c1245503..f7398465c0 100644 --- a/internal/api/ui/login/static/i18n/sv.yaml +++ b/internal/api/ui/login/static/i18n/sv.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Kön Female: Man Male: Kvinna @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Användarvillkor TosConfirm: Jag accepterar TosLinkText: Användarvillkoren @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Tillgång från hårdvaruenhet UserCode: diff --git a/internal/api/ui/login/static/i18n/zh.yaml b/internal/api/ui/login/static/i18n/zh.yaml index 4fcb469831..4ba5904700 100644 --- a/internal/api/ui/login/static/i18n/zh.yaml +++ b/internal/api/ui/login/static/i18n/zh.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: 性别 Female: 女性 Male: 男性 @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: 条款和条款 TosConfirm: 我接受 TosLinkText: 服务条款 @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: 设备授权 UserCode: diff --git a/internal/api/ui/login/static/templates/external_not_found_option.html b/internal/api/ui/login/static/templates/external_not_found_option.html index 33bcaeb4e0..dca4bd5edc 100644 --- a/internal/api/ui/login/static/templates/external_not_found_option.html +++ b/internal/api/ui/login/static/templates/external_not_found_option.html @@ -100,6 +100,8 @@ +
diff --git a/internal/auth/repository/eventsourcing/eventstore/org.go b/internal/auth/repository/eventsourcing/eventstore/org.go index 938f0d27cd..78c69d63c9 100644 --- a/internal/auth/repository/eventsourcing/eventstore/org.go +++ b/internal/auth/repository/eventsourcing/eventstore/org.go @@ -23,7 +23,7 @@ type OrgRepository struct { } func (repo *OrgRepository) GetMyPasswordComplexityPolicy(ctx context.Context) (*iam_model.PasswordComplexityPolicyView, error) { - policy, err := repo.Query.PasswordComplexityPolicyByOrg(ctx, true, authz.GetCtxData(ctx).OrgID, false) + policy, err := repo.Query.PasswordComplexityPolicyByOrg(ctx, false, authz.GetCtxData(ctx).OrgID, false) if err != nil { return nil, err } diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go index 0d87ab06bb..74a27a8312 100644 --- a/internal/auth/repository/eventsourcing/handler/handler.go +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -2,8 +2,12 @@ package handler import ( "context" + "fmt" "time" + "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 +76,13 @@ func Projections() []*handler2.Handler { } 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 auth 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("auth projection done") } return nil } diff --git a/internal/auth/repository/eventsourcing/view/view.go b/internal/auth/repository/eventsourcing/view/view.go index 56e2676b87..c67844dbad 100644 --- a/internal/auth/repository/eventsourcing/view/view.go +++ b/internal/auth/repository/eventsourcing/view/view.go @@ -1,11 +1,8 @@ package view import ( - "context" - "github.com/jinzhu/gorm" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" @@ -37,7 +34,3 @@ func StartView(sqlClient *database.DB, keyAlgorithm crypto.EncryptionAlgorithm, func (v *View) Health() (err error) { return v.Db.DB().Ping() } - -func (v *View) TimeTravel(ctx context.Context, tableName string) string { - return tableName + v.client.Timetravel(call.Took(ctx)) -} diff --git a/internal/authz/repository/eventsourcing/view/view.go b/internal/authz/repository/eventsourcing/view/view.go index f25b764f53..21a15c45fc 100644 --- a/internal/authz/repository/eventsourcing/view/view.go +++ b/internal/authz/repository/eventsourcing/view/view.go @@ -1,11 +1,8 @@ package view import ( - "context" - "github.com/jinzhu/gorm" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/query" ) @@ -31,7 +28,3 @@ func StartView(sqlClient *database.DB, queries *query.Queries) (*View, error) { func (v *View) Health() (err error) { return v.Db.DB().Ping() } - -func (v *View) TimeTravel(ctx context.Context, tableName string) string { - return tableName + v.client.Timetravel(call.Took(ctx)) -} diff --git a/internal/cache/connector/pg/connector.go b/internal/cache/connector/pg/connector.go index 9a89cf5f6a..e919aea49d 100644 --- a/internal/cache/connector/pg/connector.go +++ b/internal/cache/connector/pg/connector.go @@ -12,8 +12,7 @@ type Config struct { type Connector struct { PGXPool - Dialect string - Config Config + Config Config } func NewConnector(config Config, client *database.DB) *Connector { @@ -22,7 +21,6 @@ func NewConnector(config Config, client *database.DB) *Connector { } return &Connector{ PGXPool: client.Pool, - Dialect: client.Type(), Config: config, } } diff --git a/internal/cache/connector/pg/pg.go b/internal/cache/connector/pg/pg.go index 8469a6ef6e..530205f871 100644 --- a/internal/cache/connector/pg/pg.go +++ b/internal/cache/connector/pg/pg.go @@ -58,10 +58,8 @@ func NewCache[I ~int, K ~string, V cache.Entry[I, K]](ctx context.Context, purpo } c.logger.InfoContext(ctx, "pg cache logging enabled") - if connector.Dialect == "postgres" { - if err := c.createPartition(ctx); err != nil { - return nil, err - } + if err := c.createPartition(ctx); err != nil { + return nil, err } return c, nil } diff --git a/internal/cache/connector/pg/pg_test.go b/internal/cache/connector/pg/pg_test.go index f5980ad845..bb9b681b15 100644 --- a/internal/cache/connector/pg/pg_test.go +++ b/internal/cache/connector/pg/pg_test.go @@ -78,7 +78,6 @@ func TestNewCache(t *testing.T) { tt.expect(pool) connector := &Connector{ PGXPool: pool, - Dialect: "postgres", } c, err := NewCache[testIndex, string, *testObject](context.Background(), cachePurpose, conf, testIndices, connector) @@ -518,7 +517,6 @@ func prepareCache(t *testing.T, conf cache.Config) (cache.PrunerCache[testIndex, WillReturnResult(pgxmock.NewResult("CREATE TABLE", 0)) connector := &Connector{ PGXPool: pool, - Dialect: "postgres", } c, err := NewCache[testIndex, string, *testObject](context.Background(), cachePurpose, conf, testIndices, connector) require.NoError(t, err) diff --git a/internal/command/action_v2_execution_test.go b/internal/command/action_v2_execution_test.go index 6833125a0a..d41ea3f2d5 100644 --- a/internal/command/action_v2_execution_test.go +++ b/internal/command/action_v2_execution_test.go @@ -20,6 +20,7 @@ func existsMock(exists bool) func(method string) bool { return exists } } + func TestCommands_SetExecutionRequest(t *testing.T) { type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore diff --git a/internal/command/action_v2_target.go b/internal/command/action_v2_target.go index 95dd097ed0..3fb374b36f 100644 --- a/internal/command/action_v2_target.go +++ b/internal/command/action_v2_target.go @@ -40,31 +40,31 @@ func (a *AddTarget) IsValid() error { return nil } -func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner string) (_ *domain.ObjectDetails, err error) { +func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner string) (_ time.Time, err error) { if resourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-brml926e2d", "Errors.IDMissing") + return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-brml926e2d", "Errors.IDMissing") } if err := add.IsValid(); err != nil { - return nil, err + return time.Time{}, err } if add.AggregateID == "" { add.AggregateID, err = c.idGenerator.Next() if err != nil { - return nil, err + return time.Time{}, err } } wm, err := c.getTargetWriteModelByID(ctx, add.AggregateID, resourceOwner) if err != nil { - return nil, err + return time.Time{}, err } if wm.State.Exists() { - return nil, zerrors.ThrowAlreadyExists(nil, "INSTANCE-9axkz0jvzm", "Errors.Target.AlreadyExists") + return time.Time{}, zerrors.ThrowAlreadyExists(nil, "INSTANCE-9axkz0jvzm", "Errors.Target.AlreadyExists") } code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.targetEncryption) //nolint if err != nil { - return nil, err + return time.Time{}, err } add.SigningKey = code.PlainCode() pushedEvents, err := c.eventstore.Push(ctx, target.NewAddedEvent( @@ -78,12 +78,12 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner code.Crypted, )) if err != nil { - return nil, err + return time.Time{}, err } if err := AppendAndReduce(wm, pushedEvents...); err != nil { - return nil, err + return time.Time{}, err } - return writeModelToObjectDetails(&wm.WriteModel), nil + return wm.ChangeDate, nil } type ChangeTarget struct { @@ -118,26 +118,26 @@ func (a *ChangeTarget) IsValid() error { return nil } -func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resourceOwner string) (*domain.ObjectDetails, error) { +func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resourceOwner string) (time.Time, error) { if resourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-zqibgg0wwh", "Errors.IDMissing") + return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-zqibgg0wwh", "Errors.IDMissing") } if err := change.IsValid(); err != nil { - return nil, err + return time.Time{}, err } existing, err := c.getTargetWriteModelByID(ctx, change.AggregateID, resourceOwner) if err != nil { - return nil, err + return time.Time{}, err } if !existing.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-xj14f2cccn", "Errors.Target.NotFound") + return time.Time{}, zerrors.ThrowNotFound(nil, "COMMAND-xj14f2cccn", "Errors.Target.NotFound") } var changedSigningKey *crypto.CryptoValue if change.ExpirationSigningKey { code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.targetEncryption) //nolint if err != nil { - return nil, err + return time.Time{}, err } changedSigningKey = code.Crypted change.SigningKey = &code.Plain @@ -154,30 +154,30 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou changedSigningKey, ) if changedEvent == nil { - return writeModelToObjectDetails(&existing.WriteModel), nil + return existing.WriteModel.ChangeDate, nil } pushedEvents, err := c.eventstore.Push(ctx, changedEvent) if err != nil { - return nil, err + return time.Time{}, err } err = AppendAndReduce(existing, pushedEvents...) if err != nil { - return nil, err + return time.Time{}, err } - return writeModelToObjectDetails(&existing.WriteModel), nil + return existing.WriteModel.ChangeDate, nil } -func (c *Commands) DeleteTarget(ctx context.Context, id, resourceOwner string) (*domain.ObjectDetails, error) { +func (c *Commands) DeleteTarget(ctx context.Context, id, resourceOwner string) (time.Time, error) { if id == "" || resourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-obqos2l3no", "Errors.IDMissing") + return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-obqos2l3no", "Errors.IDMissing") } existing, err := c.getTargetWriteModelByID(ctx, id, resourceOwner) if err != nil { - return nil, err + return time.Time{}, err } if !existing.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-k4s7ucu0ax", "Errors.Target.NotFound") + return existing.WriteModel.ChangeDate, nil } if err := c.pushAppendAndReduce(ctx, @@ -187,9 +187,9 @@ func (c *Commands) DeleteTarget(ctx context.Context, id, resourceOwner string) ( existing.Name, ), ); err != nil { - return nil, err + return time.Time{}, err } - return writeModelToObjectDetails(&existing.WriteModel), nil + return existing.WriteModel.ChangeDate, nil } func (c *Commands) existsTargetsByIDs(ctx context.Context, ids []string, resourceOwner string) bool { diff --git a/internal/command/action_v2_target_test.go b/internal/command/action_v2_target_test.go index ed7d6163a0..32ecbff93a 100644 --- a/internal/command/action_v2_target_test.go +++ b/internal/command/action_v2_target_test.go @@ -31,9 +31,8 @@ func TestCommands_AddTarget(t *testing.T) { resourceOwner string } type res struct { - id string - details *domain.ObjectDetails - err func(error) bool + id string + err func(error) bool } tests := []struct { name string @@ -213,10 +212,6 @@ func TestCommands_AddTarget(t *testing.T) { }, res{ id: "id1", - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - ID: "id1", - }, }, }, { @@ -249,10 +244,6 @@ func TestCommands_AddTarget(t *testing.T) { }, res{ id: "id1", - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - ID: "id1", - }, }, }, } @@ -264,7 +255,7 @@ func TestCommands_AddTarget(t *testing.T) { newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, defaultSecretGenerators: tt.fields.defaultSecretGenerators, } - details, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner) + _, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -273,7 +264,6 @@ func TestCommands_AddTarget(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, tt.args.add.AggregateID) - assertObjectDetails(t, tt.res.details, details) } }) } @@ -291,8 +281,7 @@ func TestCommands_ChangeTarget(t *testing.T) { resourceOwner string } type res struct { - details *domain.ObjectDetails - err func(error) bool + err func(error) bool } tests := []struct { name string @@ -434,12 +423,7 @@ func TestCommands_ChangeTarget(t *testing.T) { }, resourceOwner: "instance", }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - ID: "id1", - }, - }, + res{}, }, { "unique constraint failed, error", @@ -504,12 +488,7 @@ func TestCommands_ChangeTarget(t *testing.T) { }, resourceOwner: "instance", }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - ID: "id1", - }, - }, + res{}, }, { "push full ok", @@ -557,12 +536,7 @@ func TestCommands_ChangeTarget(t *testing.T) { }, resourceOwner: "instance", }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - ID: "id1", - }, - }, + res{}, }, } for _, tt := range tests { @@ -572,16 +546,13 @@ func TestCommands_ChangeTarget(t *testing.T) { newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, defaultSecretGenerators: tt.fields.defaultSecretGenerators, } - details, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner) + _, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } if tt.res.err != nil && !tt.res.err(err) { t.Errorf("got wrong err: %v ", err) } - if tt.res.err == nil { - assertObjectDetails(t, tt.res.details, details) - } }) } } @@ -596,8 +567,7 @@ func TestCommands_DeleteTarget(t *testing.T) { resourceOwner string } type res struct { - details *domain.ObjectDetails - err func(error) bool + err func(error) bool } tests := []struct { name string @@ -631,9 +601,7 @@ func TestCommands_DeleteTarget(t *testing.T) { id: "id1", resourceOwner: "instance", }, - res{ - err: zerrors.IsNotFound, - }, + res{}, }, { "remove ok", @@ -657,12 +625,31 @@ func TestCommands_DeleteTarget(t *testing.T) { id: "id1", resourceOwner: "instance", }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - ID: "id1", - }, + res{}, + }, + { + "already removed", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + targetAddEvent("id1", "instance"), + ), + eventFromEventPusher( + target.NewRemovedEvent(context.Background(), + target.NewAggregate("id1", "instance"), + "name", + ), + ), + ), + ), }, + args{ + ctx: context.Background(), + id: "id1", + resourceOwner: "instance", + }, + res{}, }, } for _, tt := range tests { @@ -670,16 +657,13 @@ func TestCommands_DeleteTarget(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore(t), } - details, err := c.DeleteTarget(tt.args.ctx, tt.args.id, tt.args.resourceOwner) + _, err := c.DeleteTarget(tt.args.ctx, tt.args.id, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } if tt.res.err != nil && !tt.res.err(err) { t.Errorf("got wrong err: %v ", err) } - if tt.res.err == nil { - assertObjectDetails(t, tt.res.details, details) - } }) } } diff --git a/internal/command/auth_request.go b/internal/command/auth_request.go index 340155d11b..d60012637a 100644 --- a/internal/command/auth_request.go +++ b/internal/command/auth_request.go @@ -29,6 +29,7 @@ type AuthRequest struct { LoginHint *string HintUserID *string NeedRefreshToken bool + Issuer string } type CurrentAuthRequest struct { @@ -73,6 +74,7 @@ func (c *Commands) AddAuthRequest(ctx context.Context, authRequest *AuthRequest) authRequest.LoginHint, authRequest.HintUserID, authRequest.NeedRefreshToken, + authRequest.Issuer, )) if err != nil { return nil, err @@ -180,6 +182,7 @@ func authRequestWriteModelToCurrentAuthRequest(writeModel *AuthRequestWriteModel MaxAge: writeModel.MaxAge, LoginHint: writeModel.LoginHint, HintUserID: writeModel.HintUserID, + Issuer: writeModel.Issuer, }, SessionID: writeModel.SessionID, UserID: writeModel.UserID, diff --git a/internal/command/auth_request_model.go b/internal/command/auth_request_model.go index a6766d1979..0e8d88fd13 100644 --- a/internal/command/auth_request_model.go +++ b/internal/command/auth_request_model.go @@ -36,6 +36,7 @@ type AuthRequestWriteModel struct { AuthMethods []domain.UserAuthMethodType AuthRequestState domain.AuthRequestState NeedRefreshToken bool + Issuer string } func NewAuthRequestWriteModel(ctx context.Context, id string) *AuthRequestWriteModel { @@ -68,6 +69,7 @@ func (m *AuthRequestWriteModel) Reduce() error { m.HintUserID = e.HintUserID m.AuthRequestState = domain.AuthRequestStateAdded m.NeedRefreshToken = e.NeedRefreshToken + m.Issuer = e.Issuer case *authrequest.SessionLinkedEvent: m.SessionID = e.SessionID m.UserID = e.UserID diff --git a/internal/command/auth_request_test.go b/internal/command/auth_request_test.go index 590e4086f4..c0b5f630f7 100644 --- a/internal/command/auth_request_test.go +++ b/internal/command/auth_request_test.go @@ -62,6 +62,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { nil, nil, false, + "issuer", ), ), ), @@ -101,6 +102,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), false, + "issuer", ), ), ), @@ -127,6 +129,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { MaxAge: gu.Ptr(time.Duration(0)), LoginHint: gu.Ptr("loginHint"), HintUserID: gu.Ptr("hintUserID"), + Issuer: "issuer", }, }, &CurrentAuthRequest{ @@ -150,6 +153,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { MaxAge: gu.Ptr(time.Duration(0)), LoginHint: gu.Ptr("loginHint"), HintUserID: gu.Ptr("hintUserID"), + Issuer: "issuer", }, }, nil, @@ -234,6 +238,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), eventFromEventPusher( @@ -276,6 +281,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -317,6 +323,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -356,6 +363,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -418,6 +426,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -469,6 +478,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -527,6 +537,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, SessionID: "sessionID", UserID: "userID", @@ -557,6 +568,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -616,6 +628,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, SessionID: "sessionID", UserID: "userID", @@ -646,6 +659,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -706,6 +720,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, SessionID: "sessionID", UserID: "userID", @@ -736,6 +751,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -797,6 +813,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, SessionID: "sessionID", UserID: "userID", @@ -827,6 +844,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -950,6 +968,7 @@ func TestCommands_FailAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -978,6 +997,7 @@ func TestCommands_FailAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, }, }, @@ -1050,6 +1070,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), ), @@ -1088,6 +1109,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( diff --git a/internal/command/command.go b/internal/command/command.go index 17f6641caf..b0e67ad52e 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -9,7 +9,9 @@ import ( "fmt" "math/big" "net/http" + "slices" "strconv" + "strings" "sync" "time" @@ -177,13 +179,18 @@ func StartCommands( defaultSecretGenerators: defaultSecretGenerators, samlCertificateAndKeyGenerator: samlCertificateAndKeyGenerator(defaults.KeyConfig.CertificateSize, defaults.KeyConfig.CertificateLifetime), webKeyGenerator: crypto.GenerateEncryptedWebKey, - // always true for now until we can check with an eventlist - EventExisting: func(event string) bool { return true }, - // always true for now until we can check with an eventlist - EventGroupExisting: func(group string) bool { return true }, + EventExisting: func(value string) bool { + return slices.Contains(es.EventTypes(), value) + }, + EventGroupExisting: func(group string) bool { + return slices.ContainsFunc(es.EventTypes(), func(value string) bool { + return strings.HasPrefix(value, group) + }, + ) + }, GrpcServiceExisting: func(service string) bool { return false }, GrpcMethodExisting: func(method string) bool { return false }, - ActionFunctionExisting: domain.FunctionExists(), + ActionFunctionExisting: domain.ActionFunctionExists(), multifactors: domain.MultifactorConfigs{ OTP: domain.OTPConfig{ CryptoMFA: otpEncryption, @@ -218,33 +225,6 @@ func (c *Commands) pushAppendAndReduce(ctx context.Context, object AppendReducer return AppendAndReduce(object, events...) } -// pushChunked pushes the commands in chunks of size to the eventstore. -// This can be used to reduce the amount of events in a single transaction. -// When an error occurs, the events that have been pushed so far will be returned. -// -// Warning: chunks are pushed in separate transactions. -// Successful pushes will not be rolled back if a later chunk fails. -// Only use this function when the caller is able to handle partial success -// and is able to consolidate the state on errors. -func (c *Commands) pushChunked(ctx context.Context, size uint16, cmds ...eventstore.Command) (_ []eventstore.Event, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - events := make([]eventstore.Event, 0, len(cmds)) - for i := 0; i < len(cmds); i += int(size) { - end := i + int(size) - if end > len(cmds) { - end = len(cmds) - } - chunk, err := c.eventstore.Push(ctx, cmds[i:end]...) - if err != nil { - return events, err - } - events = append(events, chunk...) - } - return events, nil -} - type AppendReducerDetails interface { AppendEvents(...eventstore.Event) // TODO: Why is it allowed to return an error here? diff --git a/internal/command/command_test.go b/internal/command/command_test.go index 7224f047b5..2367930b89 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -2,7 +2,6 @@ package command import ( "context" - "fmt" "io" "os" "testing" @@ -14,7 +13,6 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/repository/permission" "github.com/zitadel/zitadel/internal/repository/user" ) @@ -31,93 +29,6 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func TestCommands_pushChunked(t *testing.T) { - aggregate := permission.NewAggregate("instanceID") - cmds := make([]eventstore.Command, 100) - for i := 0; i < 100; i++ { - cmds[i] = permission.NewAddedEvent(context.Background(), aggregate, "role", fmt.Sprintf("permission%d", i)) - } - type args struct { - size uint16 - } - tests := []struct { - name string - args args - eventstore func(*testing.T) *eventstore.Eventstore - wantEvents int - wantErr error - }{ - { - name: "push error", - args: args{ - size: 100, - }, - eventstore: expectEventstore( - expectPushFailed(io.ErrClosedPipe, cmds...), - ), - wantEvents: 0, - wantErr: io.ErrClosedPipe, - }, - { - name: "single chunk", - args: args{ - size: 100, - }, - eventstore: expectEventstore( - expectPush(cmds...), - ), - wantEvents: len(cmds), - }, - { - name: "aligned chunks", - args: args{ - size: 50, - }, - eventstore: expectEventstore( - expectPush(cmds[0:50]...), - expectPush(cmds[50:100]...), - ), - wantEvents: len(cmds), - }, - { - name: "odd chunks", - args: args{ - size: 30, - }, - eventstore: expectEventstore( - expectPush(cmds[0:30]...), - expectPush(cmds[30:60]...), - expectPush(cmds[60:90]...), - expectPush(cmds[90:100]...), - ), - wantEvents: len(cmds), - }, - { - name: "partial error", - args: args{ - size: 30, - }, - eventstore: expectEventstore( - expectPush(cmds[0:30]...), - expectPush(cmds[30:60]...), - expectPushFailed(io.ErrClosedPipe, cmds[60:90]...), - ), - wantEvents: len(cmds[0:60]), - wantErr: io.ErrClosedPipe, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Commands{ - eventstore: tt.eventstore(t), - } - gotEvents, err := c.pushChunked(context.Background(), tt.args.size, cmds...) - require.ErrorIs(t, err, tt.wantErr) - assert.Len(t, gotEvents, tt.wantEvents) - }) - } -} - func TestCommands_asyncPush(t *testing.T) { // make sure the test terminates on deadlock background := context.Background() diff --git a/internal/command/instance_features.go b/internal/command/instance_features.go index 3e927cc0c5..cb12bff828 100644 --- a/internal/command/instance_features.go +++ b/internal/command/instance_features.go @@ -21,7 +21,6 @@ type InstanceFeatures struct { LegacyIntrospection *bool UserSchema *bool TokenExchange *bool - Actions *bool ImprovedPerformance []feature.ImprovedPerformanceType WebKey *bool DebugOIDCParentError *bool @@ -39,7 +38,6 @@ func (m *InstanceFeatures) isEmpty() bool { m.LegacyIntrospection == nil && m.UserSchema == nil && m.TokenExchange == nil && - m.Actions == nil && // nil check to allow unset improvements m.ImprovedPerformance == nil && m.WebKey == nil && diff --git a/internal/command/instance_features_model.go b/internal/command/instance_features_model.go index 954c769304..977a46b6c2 100644 --- a/internal/command/instance_features_model.go +++ b/internal/command/instance_features_model.go @@ -71,7 +71,6 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceLegacyIntrospectionEventType, feature_v2.InstanceUserSchemaEventType, feature_v2.InstanceTokenExchangeEventType, - feature_v2.InstanceActionsEventType, feature_v2.InstanceImprovedPerformanceEventType, feature_v2.InstanceWebKeyEventType, feature_v2.InstanceDebugOIDCParentErrorEventType, @@ -108,9 +107,6 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an case feature.KeyUserSchema: v := value.(bool) features.UserSchema = &v - case feature.KeyActions: - v := value.(bool) - features.Actions = &v case feature.KeyImprovedPerformance: v := value.([]feature.ImprovedPerformanceType) features.ImprovedPerformance = v @@ -148,7 +144,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.InstanceLegacyIntrospectionEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.InstanceTokenExchangeEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.InstanceUserSchemaEventType) - cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.InstanceActionsEventType) cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.InstanceImprovedPerformanceEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.WebKey, f.WebKey, feature_v2.InstanceWebKeyEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DebugOIDCParentError, f.DebugOIDCParentError, feature_v2.InstanceDebugOIDCParentErrorEventType) diff --git a/internal/command/instance_features_test.go b/internal/command/instance_features_test.go index e6b6bb4346..02e8896a0c 100644 --- a/internal/command/instance_features_test.go +++ b/internal/command/instance_features_test.go @@ -149,24 +149,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ResourceOwner: "instance1", }, }, - { - name: "set Actions", - eventstore: expectEventstore( - expectFilter(), - expectPush( - feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceActionsEventType, true, - ), - ), - ), - args: args{ctx, &InstanceFeatures{ - Actions: gu.Ptr(true), - }}, - want: &domain.ObjectDetails{ - ResourceOwner: "instance1", - }, - }, { name: "push error", eventstore: expectEventstore( @@ -204,10 +186,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceUserSchemaEventType, true, ), - feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceActionsEventType, true, - ), feature_v2.NewSetEvent[bool]( ctx, aggregate, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, true, @@ -219,7 +197,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: gu.Ptr(true), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), OIDCSingleV1SessionTermination: gu.Ptr(true), }}, want: &domain.ObjectDetails{ diff --git a/internal/command/instance_role_permissions.go b/internal/command/instance_role_permissions.go index c0c6355dd6..fce272cc12 100644 --- a/internal/command/instance_role_permissions.go +++ b/internal/command/instance_role_permissions.go @@ -17,15 +17,9 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -const ( - CockroachRollPermissionChunkSize uint16 = 50 -) - // SynchronizeRolePermission checks the current state of role permissions in the eventstore for the aggregate. // It pushes the commands required to reach the desired state passed in target. // For system level permissions aggregateID must be set to `SYSTEM`, else it is the instance ID. -// -// In case cockroachDB is used, the commands are pushed in chunks of CockroachRollPermissionChunkSize. func (c *Commands) SynchronizeRolePermission(ctx context.Context, aggregateID string, target []authz.RoleMapping) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -36,13 +30,9 @@ func (c *Commands) SynchronizeRolePermission(ctx context.Context, aggregateID st if err != nil { return nil, zerrors.ThrowInternal(err, "COMMA-Iej2r", "Errors.Internal") } - var events []eventstore.Event - if c.eventstore.Client().Database.Type() == "cockroach" { - events, err = c.pushChunked(ctx, CockroachRollPermissionChunkSize, cmds...) - } else { - events, err = c.eventstore.Push(ctx, cmds...) - } + events, err := c.eventstore.Push(ctx, cmds...) if err != nil { + logging.WithError(err).Error("failed to push role permission commands") return nil, zerrors.ThrowInternal(err, "COMMA-AiV3u", "Errors.Internal") } return pushedEventsToObjectDetails(events), nil diff --git a/internal/command/oidc_session_test.go b/internal/command/oidc_session_test.go index af1874a6bb..564c39460b 100644 --- a/internal/command/oidc_session_test.go +++ b/internal/command/oidc_session_test.go @@ -138,6 +138,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -182,6 +183,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -234,6 +236,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -331,6 +334,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -465,6 +469,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -610,6 +615,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -748,6 +754,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), false, + "issuer", ), ), eventFromEventPusher( diff --git a/internal/command/project_application_saml_test.go b/internal/command/project_application_saml_test.go index 3082e87c46..c6f6f7cf21 100644 --- a/internal/command/project_application_saml_test.go +++ b/internal/command/project_application_saml_test.go @@ -3,7 +3,7 @@ package command import ( "bytes" "context" - "io/ioutil" + "io" "net/http" "testing" @@ -882,7 +882,7 @@ func newTestClient(httpStatus int, metadata []byte) *http.Client { fn := roundTripperFunc(func(req *http.Request) *http.Response { return &http.Response{ StatusCode: httpStatus, - Body: ioutil.NopCloser(bytes.NewBuffer(metadata)), + Body: io.NopCloser(bytes.NewBuffer(metadata)), Header: make(http.Header), //must be non-nil value } }) diff --git a/internal/command/saml_request.go b/internal/command/saml_request.go index 17f56101ec..40e0643f0c 100644 --- a/internal/command/saml_request.go +++ b/internal/command/saml_request.go @@ -15,13 +15,14 @@ type SAMLRequest struct { ID string LoginClient string - ApplicationID string - ACSURL string - RelayState string - RequestID string - Binding string - Issuer string - Destination string + ApplicationID string + ACSURL string + RelayState string + RequestID string + Binding string + Issuer string + Destination string + ResponseIssuer string } type CurrentSAMLRequest struct { @@ -56,6 +57,7 @@ func (c *Commands) AddSAMLRequest(ctx context.Context, samlRequest *SAMLRequest) samlRequest.Binding, samlRequest.Issuer, samlRequest.Destination, + samlRequest.ResponseIssuer, )) if err != nil { return nil, err @@ -131,15 +133,16 @@ func (c *Commands) FailSAMLRequest(ctx context.Context, id string, reason domain func samlRequestWriteModelToCurrentSAMLRequest(writeModel *SAMLRequestWriteModel) (_ *CurrentSAMLRequest) { return &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: writeModel.AggregateID, - LoginClient: writeModel.LoginClient, - ApplicationID: writeModel.ApplicationID, - ACSURL: writeModel.ACSURL, - RelayState: writeModel.RelayState, - RequestID: writeModel.RequestID, - Binding: writeModel.Binding, - Issuer: writeModel.Issuer, - Destination: writeModel.Destination, + ID: writeModel.AggregateID, + LoginClient: writeModel.LoginClient, + ApplicationID: writeModel.ApplicationID, + ACSURL: writeModel.ACSURL, + RelayState: writeModel.RelayState, + RequestID: writeModel.RequestID, + Binding: writeModel.Binding, + Issuer: writeModel.Issuer, + Destination: writeModel.Destination, + ResponseIssuer: writeModel.ResponseIssuer, }, SessionID: writeModel.SessionID, UserID: writeModel.UserID, diff --git a/internal/command/saml_request_model.go b/internal/command/saml_request_model.go index 7ba640cbe8..afd5b052c7 100644 --- a/internal/command/saml_request_model.go +++ b/internal/command/saml_request_model.go @@ -15,14 +15,15 @@ type SAMLRequestWriteModel struct { eventstore.WriteModel aggregate *eventstore.Aggregate - LoginClient string - ApplicationID string - ACSURL string - RelayState string - RequestID string - Binding string - Issuer string - Destination string + LoginClient string + ApplicationID string + ACSURL string + RelayState string + RequestID string + Binding string + Issuer string + Destination string + ResponseIssuer string SessionID string UserID string @@ -52,6 +53,7 @@ func (m *SAMLRequestWriteModel) Reduce() error { m.Binding = e.Binding m.Issuer = e.Issuer m.Destination = e.Destination + m.ResponseIssuer = e.ResponseIssuer m.SAMLRequestState = domain.SAMLRequestStateAdded case *samlrequest.SessionLinkedEvent: m.SessionID = e.SessionID diff --git a/internal/command/saml_request_test.go b/internal/command/saml_request_test.go index 761edde8fb..c11c87ec48 100644 --- a/internal/command/saml_request_test.go +++ b/internal/command/saml_request_test.go @@ -54,6 +54,7 @@ func TestCommands_AddSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -82,6 +83,7 @@ func TestCommands_AddSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -90,27 +92,29 @@ func TestCommands_AddSAMLRequest(t *testing.T) { args{ ctx: mockCtx, request: &SAMLRequest{ - LoginClient: "login", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + LoginClient: "login", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, }, &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "login", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "login", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, }, nil, @@ -187,6 +191,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), eventFromEventPusher( @@ -222,6 +227,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -255,6 +261,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -286,6 +293,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -340,6 +348,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -383,6 +392,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -431,15 +441,16 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, authReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "login", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "login", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, SessionID: "sessionID", UserID: "userID", @@ -462,6 +473,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -511,15 +523,16 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, authReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "loginClient", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "loginClient", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, SessionID: "sessionID", UserID: "userID", @@ -541,6 +554,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -591,15 +605,16 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, authReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "loginClient", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "loginClient", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, SessionID: "sessionID", UserID: "userID", @@ -622,6 +637,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -672,15 +688,16 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, authReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "loginClient", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "loginClient", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, SessionID: "sessionID", UserID: "userID", @@ -703,6 +720,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -817,6 +835,7 @@ func TestCommands_FailSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), samlrequest.NewFailedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate, @@ -850,6 +869,7 @@ func TestCommands_FailSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -870,15 +890,16 @@ func TestCommands_FailSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, samlReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "login", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "login", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, }, }, diff --git a/internal/command/saml_session_test.go b/internal/command/saml_session_test.go index 12cc0683c5..4781381cc4 100644 --- a/internal/command/saml_session_test.go +++ b/internal/command/saml_session_test.go @@ -99,6 +99,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -129,6 +130,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), eventFromEventPusher( @@ -167,6 +169,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), eventFromEventPusher( @@ -248,6 +251,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), eventFromEventPusher( diff --git a/internal/command/system_features.go b/internal/command/system_features.go index dc886de318..b317ea93bb 100644 --- a/internal/command/system_features.go +++ b/internal/command/system_features.go @@ -15,7 +15,6 @@ type SystemFeatures struct { LegacyIntrospection *bool TokenExchange *bool UserSchema *bool - Actions *bool ImprovedPerformance []feature.ImprovedPerformanceType OIDCSingleV1SessionTermination *bool DisableUserTokenEvent *bool @@ -30,7 +29,6 @@ func (m *SystemFeatures) isEmpty() bool { m.LegacyIntrospection == nil && m.UserSchema == nil && m.TokenExchange == nil && - m.Actions == nil && // nil check to allow unset improvements m.ImprovedPerformance == nil && m.OIDCSingleV1SessionTermination == nil && diff --git a/internal/command/system_features_model.go b/internal/command/system_features_model.go index 15fc3e0bf0..28e56f8bd4 100644 --- a/internal/command/system_features_model.go +++ b/internal/command/system_features_model.go @@ -64,7 +64,6 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v2.SystemLegacyIntrospectionEventType, feature_v2.SystemUserSchemaEventType, feature_v2.SystemTokenExchangeEventType, - feature_v2.SystemActionsEventType, feature_v2.SystemImprovedPerformanceEventType, feature_v2.SystemOIDCSingleV1SessionTerminationEventType, feature_v2.SystemDisableUserTokenEvent, @@ -98,9 +97,6 @@ func reduceSystemFeature(features *SystemFeatures, key feature.Key, value any) { case feature.KeyTokenExchange: v := value.(bool) features.TokenExchange = &v - case feature.KeyActions: - v := value.(bool) - features.Actions = &v case feature.KeyImprovedPerformance: features.ImprovedPerformance = value.([]feature.ImprovedPerformanceType) case feature.KeyOIDCSingleV1SessionTermination: @@ -128,7 +124,6 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.SystemLegacyIntrospectionEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.SystemUserSchemaEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.SystemTokenExchangeEventType) - cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.SystemActionsEventType) cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.SystemImprovedPerformanceEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.OIDCSingleV1SessionTermination, f.OIDCSingleV1SessionTermination, feature_v2.SystemOIDCSingleV1SessionTerminationEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DisableUserTokenEvent, f.DisableUserTokenEvent, feature_v2.SystemDisableUserTokenEvent) diff --git a/internal/command/system_features_test.go b/internal/command/system_features_test.go index 9c5f4cc2a9..b1b5207b8c 100644 --- a/internal/command/system_features_test.go +++ b/internal/command/system_features_test.go @@ -117,24 +117,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { ResourceOwner: "SYSTEM", }, }, - { - name: "set Actions", - eventstore: expectEventstore( - expectFilter(), - expectPush( - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, true, - ), - ), - ), - args: args{context.Background(), &SystemFeatures{ - Actions: gu.Ptr(true), - }}, - want: &domain.ObjectDetails{ - ResourceOwner: "SYSTEM", - }, - }, { name: "push error", eventstore: expectEventstore( @@ -172,10 +154,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, true, ), - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, true, - ), feature_v2.NewSetEvent[bool]( context.Background(), aggregate, feature_v2.SystemOIDCSingleV1SessionTerminationEventType, true, @@ -187,7 +165,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: gu.Ptr(true), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), OIDCSingleV1SessionTermination: gu.Ptr(true), }}, want: &domain.ObjectDetails{ @@ -233,10 +210,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, true, ), - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, false, - ), feature_v2.NewSetEvent[bool]( context.Background(), aggregate, feature_v2.SystemOIDCSingleV1SessionTerminationEventType, false, @@ -248,7 +221,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: gu.Ptr(true), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(false), OIDCSingleV1SessionTermination: gu.Ptr(false), }}, want: &domain.ObjectDetails{ diff --git a/internal/command/web_key.go b/internal/command/web_key.go index e8481541c3..b46d5fc1fc 100644 --- a/internal/command/web_key.go +++ b/internal/command/web_key.go @@ -2,6 +2,7 @@ package command import ( "context" + "time" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" @@ -156,27 +157,28 @@ func (c *Commands) getAllWebKeys(ctx context.Context) (_ map[string]*WebKeyWrite return models.keys, models.activeID, nil } -func (c *Commands) DeleteWebKey(ctx context.Context, keyID string) (_ *domain.ObjectDetails, err error) { +func (c *Commands) DeleteWebKey(ctx context.Context, keyID string) (_ time.Time, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() model := NewWebKeyWriteModel(keyID, authz.GetInstance(ctx).InstanceID()) if err = c.eventstore.FilterToQueryReducer(ctx, model); err != nil { - return nil, err + return time.Time{}, err } - if model.State == domain.WebKeyStateUnspecified { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-ooCa7", "Errors.WebKey.NotFound") + if model.State == domain.WebKeyStateUnspecified || + model.State == domain.WebKeyStateRemoved { + return model.WriteModel.ChangeDate, nil } if model.State == domain.WebKeyStateActive { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Chai1", "Errors.WebKey.ActiveDelete") + return time.Time{}, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Chai1", "Errors.WebKey.ActiveDelete") } err = c.pushAppendAndReduce(ctx, model, webkey.NewRemovedEvent(ctx, webkey.AggregateFromWriteModel(ctx, &model.WriteModel), )) if err != nil { - return nil, err + return time.Time{}, err } - return writeModelToObjectDetails(&model.WriteModel), nil + return model.WriteModel.ChangeDate, nil } func (c *Commands) prepareGenerateInitialWebKeys(instanceID string, conf crypto.WebKeyConfig) preparation.Validation { diff --git a/internal/command/web_key_test.go b/internal/command/web_key_test.go index 63463de1df..13587d0f68 100644 --- a/internal/command/web_key_test.go +++ b/internal/command/web_key_test.go @@ -7,6 +7,7 @@ import ( "crypto/rand" "io" "testing" + "time" "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/assert" @@ -610,7 +611,7 @@ func TestCommands_DeleteWebKey(t *testing.T) { name string fields fields args args - want *domain.ObjectDetails + want time.Time wantErr error }{ { @@ -624,14 +625,73 @@ func TestCommands_DeleteWebKey(t *testing.T) { wantErr: io.ErrClosedPipe, }, { - name: "not found error", + name: "not found", fields: fields{ eventstore: expectEventstore( expectFilter(), ), }, - args: args{"key1"}, - wantErr: zerrors.ThrowNotFound(nil, "COMMAND-ooCa7", "Errors.WebKey.NotFound"), + args: args{"key1"}, + want: time.Time{}, + }, + { + name: "previously deleted", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key2", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key2", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key2", "instance1"), + )), + eventFromEventPusher(webkey.NewDeactivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + eventFromEventPusher(webkey.NewRemovedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + ), + ), + }, + args: args{"key1"}, + want: time.Time{}, }, { name: "key active error", @@ -722,10 +782,7 @@ func TestCommands_DeleteWebKey(t *testing.T) { ), }, args: args{"key1"}, - want: &domain.ObjectDetails{ - ResourceOwner: "instance1", - ID: "key1", - }, + want: time.Time{}, }, } for _, tt := range tests { diff --git a/internal/crypto/passwap.go b/internal/crypto/passwap.go index 6ff0f4ea10..e14c2dfaaf 100644 --- a/internal/crypto/passwap.go +++ b/internal/crypto/passwap.go @@ -12,6 +12,7 @@ import ( "github.com/zitadel/passwap/bcrypt" "github.com/zitadel/passwap/md5" "github.com/zitadel/passwap/md5plain" + "github.com/zitadel/passwap/md5salted" "github.com/zitadel/passwap/pbkdf2" "github.com/zitadel/passwap/scrypt" "github.com/zitadel/passwap/verifier" @@ -43,14 +44,15 @@ func (h *Hasher) EncodingSupported(encodedHash string) bool { type HashName string const ( - HashNameArgon2 HashName = "argon2" // used for the common argon2 verifier - HashNameArgon2i HashName = "argon2i" // hash only - HashNameArgon2id HashName = "argon2id" // hash only - HashNameBcrypt HashName = "bcrypt" // hash and verify - HashNameMd5 HashName = "md5" // verify only, as hashing with md5 is insecure and deprecated - HashNameMd5Plain HashName = "md5plain" // verify only, as hashing with md5 is insecure and deprecated - HashNameScrypt HashName = "scrypt" // hash and verify - HashNamePBKDF2 HashName = "pbkdf2" // hash and verify + HashNameArgon2 HashName = "argon2" // used for the common argon2 verifier + HashNameArgon2i HashName = "argon2i" // hash only + HashNameArgon2id HashName = "argon2id" // hash only + HashNameBcrypt HashName = "bcrypt" // hash and verify + HashNameMd5 HashName = "md5" // verify only, as hashing with md5 is insecure and deprecated + HashNameMd5Plain HashName = "md5plain" // verify only, as hashing with md5 is insecure and deprecated + HashNameMd5Salted HashName = "md5salted" // verify only, as hashing with md5 is insecure and deprecated + HashNameScrypt HashName = "scrypt" // hash and verify + HashNamePBKDF2 HashName = "pbkdf2" // hash and verify ) type HashMode string @@ -119,6 +121,10 @@ var knowVerifiers = map[HashName]prefixVerifier{ prefixes: []string{pbkdf2.Prefix}, verifier: pbkdf2.Verifier, }, + HashNameMd5Salted: { + prefixes: []string{md5salted.Prefix}, + verifier: md5salted.Verifier, + }, } func (c *HashConfig) buildVerifiers() (verifiers []verifier.Verifier, prefixes []string, err error) { diff --git a/internal/crypto/passwap_test.go b/internal/crypto/passwap_test.go index cbc7202501..b872b0e298 100644 --- a/internal/crypto/passwap_test.go +++ b/internal/crypto/passwap_test.go @@ -11,6 +11,7 @@ import ( "github.com/zitadel/passwap/argon2" "github.com/zitadel/passwap/bcrypt" "github.com/zitadel/passwap/md5" + "github.com/zitadel/passwap/md5salted" "github.com/zitadel/passwap/pbkdf2" "github.com/zitadel/passwap/scrypt" ) @@ -76,6 +77,7 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { HashNameArgon2, HashNameBcrypt, HashNameMd5, + HashNameMd5Salted, HashNameScrypt, "foobar", }, @@ -122,6 +124,24 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { }, wantErr: true, }, + { + name: "invalid md5plain", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNameMd5Plain, + }, + }, + wantErr: true, + }, + { + name: "invalid md5salted", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNameMd5Salted, + }, + }, + wantErr: true, + }, { name: "invalid argon2", fields: fields{ @@ -160,9 +180,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "threads": 4, }, }, - Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt}, + Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux}, + wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux, md5salted.Prefix}, }, { name: "argon2id, error", @@ -188,9 +208,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "threads": 4, }, }, - Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt}, + Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux}, + wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux, md5salted.Prefix}, }, { name: "bcrypt, error", @@ -213,9 +233,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "cost": 3, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameMd5, HashNameScrypt}, + Verifiers: []HashName{HashNameArgon2, HashNameMd5, HashNameScrypt, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{bcrypt.Prefix, argon2.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux}, + wantPrefixes: []string{bcrypt.Prefix, argon2.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux, md5salted.Prefix}, }, { name: "scrypt, error", @@ -238,9 +258,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "cost": 3, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5}, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + wantPrefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, { name: "pbkdf2, parse error", @@ -277,9 +297,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "Hash": HashModeSHA1, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5}, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, { name: "pbkdf2, sha224", @@ -291,9 +311,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "Hash": HashModeSHA224, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5}, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, { name: "pbkdf2, sha256", @@ -305,9 +325,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "Hash": HashModeSHA256, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5}, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, { name: "pbkdf2, sha384", @@ -319,9 +339,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "Hash": HashModeSHA384, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5}, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, { name: "pbkdf2, sha512", @@ -333,9 +353,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "Hash": HashModeSHA512, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5}, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, } for _, tt := range tests { diff --git a/internal/database/cockroach/crdb.go b/internal/database/cockroach/crdb.go index a5b3208a86..f89792c0c8 100644 --- a/internal/database/cockroach/crdb.go +++ b/internal/database/cockroach/crdb.go @@ -18,7 +18,7 @@ import ( func init() { config := new(Config) - dialect.Register(config, config, true) + dialect.Register(config, config, false) } const ( @@ -52,7 +52,7 @@ func (c *Config) MatchName(name string) bool { return false } -func (_ *Config) Decode(configs []interface{}) (dialect.Connector, error) { +func (_ *Config) Decode(configs []any) (dialect.Connector, error) { connector := new(Config) decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ DecodeHook: mapstructure.StringToTimeDurationHookFunc(), @@ -73,12 +73,6 @@ func (_ *Config) Decode(configs []interface{}) (dialect.Connector, error) { } func (c *Config) Connect(useAdmin bool) (*sql.DB, *pgxpool.Pool, error) { - dialect.RegisterAfterConnect(func(ctx context.Context, c *pgx.Conn) error { - // CockroachDB by default does not allow multiple modifications of the same table using ON CONFLICT - // This is needed to fill the fields table of the eventstore during eventstore.Push. - _, err := c.Exec(ctx, "SET enable_multiple_modifications_of_table = on") - return err - }) connConfig := dialect.NewConnectionConfig(c.MaxOpenConns, c.MaxIdleConns) config, err := pgxpool.ParseConfig(c.String(useAdmin)) @@ -149,12 +143,8 @@ func (c *Config) Password() string { return c.User.Password } -func (c *Config) Type() string { - return "cockroach" -} - -func (c *Config) Timetravel(d time.Duration) string { - return "" +func (c *Config) Type() dialect.DatabaseType { + return dialect.DatabaseTypeCockroach } type User struct { diff --git a/internal/database/database.go b/internal/database/database.go index e254edadc1..ddc26a7961 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -149,33 +149,40 @@ func Connect(config Config, useAdmin bool) (*DB, error) { }, nil } -func DecodeHook(from, to reflect.Value) (_ interface{}, err error) { - if to.Type() != reflect.TypeOf(Config{}) { - return from.Interface(), nil - } - - config := new(Config) - if err = mapstructure.Decode(from.Interface(), config); err != nil { - return nil, err - } - - configuredDialect := dialect.SelectByConfig(config.Dialects) - configs := make([]interface{}, 0, len(config.Dialects)-1) - - for name, dialectConfig := range config.Dialects { - if !configuredDialect.Matcher.MatchName(name) { - continue +func DecodeHook(allowCockroach bool) func(from, to reflect.Value) (_ interface{}, err error) { + return func(from, to reflect.Value) (_ interface{}, err error) { + if to.Type() != reflect.TypeOf(Config{}) { + return from.Interface(), nil } - configs = append(configs, dialectConfig) - } + config := new(Config) + if err = mapstructure.Decode(from.Interface(), config); err != nil { + return nil, err + } - config.connector, err = configuredDialect.Matcher.Decode(configs) - if err != nil { - return nil, err - } + configuredDialect := dialect.SelectByConfig(config.Dialects) + configs := make([]any, 0, len(config.Dialects)) - return config, nil + for name, dialectConfig := range config.Dialects { + if !configuredDialect.Matcher.MatchName(name) { + continue + } + + configs = append(configs, dialectConfig) + } + + if !allowCockroach && configuredDialect.Matcher.Type() == dialect.DatabaseTypeCockroach { + logging.Info("Cockroach support was removed with Zitadel v3, please refer to https://zitadel.com/docs/self-hosting/manage/cli/mirror to migrate your data to postgres") + return nil, zerrors.ThrowPreconditionFailed(nil, "DATAB-0pIWD", "Cockroach support was removed with Zitadel v3") + } + + config.connector, err = configuredDialect.Matcher.Decode(configs) + if err != nil { + return nil, err + } + + return config, nil + } } func (c Config) DatabaseName() string { @@ -190,7 +197,7 @@ func (c Config) Password() string { return c.connector.Password() } -func (c Config) Type() string { +func (c Config) Type() dialect.DatabaseType { return c.connector.Type() } diff --git a/internal/database/dialect/config.go b/internal/database/dialect/config.go index 71fb477ea1..16068daadd 100644 --- a/internal/database/dialect/config.go +++ b/internal/database/dialect/config.go @@ -3,7 +3,6 @@ package dialect import ( "database/sql" "sync" - "time" "github.com/jackc/pgx/v5/pgxpool" ) @@ -22,9 +21,17 @@ var ( type Matcher interface { MatchName(string) bool - Decode([]interface{}) (Connector, error) + Decode([]any) (Connector, error) + Type() DatabaseType } +type DatabaseType uint8 + +const ( + DatabaseTypePostgres DatabaseType = iota + DatabaseTypeCockroach +) + const ( DefaultAppName = "zitadel" ) @@ -38,8 +45,7 @@ type Connector interface { type Database interface { DatabaseName() string Username() string - Type() string - Timetravel(time.Duration) string + Type() DatabaseType } func Register(matcher Matcher, config Connector, isDefault bool) { diff --git a/internal/database/postgres/embedded.go b/internal/database/postgres/embedded.go new file mode 100644 index 0000000000..57aec756f0 --- /dev/null +++ b/internal/database/postgres/embedded.go @@ -0,0 +1,38 @@ +package postgres + +import ( + "net" + "os" + + embeddedpostgres "github.com/fergusstrange/embedded-postgres" + "github.com/zitadel/logging" +) + +func StartEmbedded() (embeddedpostgres.Config, func()) { + path, err := os.MkdirTemp("", "zitadel-embedded-postgres-*") + logging.OnError(err).Fatal("unable to create temp dir") + + port, close := getPort() + + config := embeddedpostgres.DefaultConfig().Version(embeddedpostgres.V16).Port(uint32(port)).RuntimePath(path) + embedded := embeddedpostgres.NewDatabase(config) + + close() + err = embedded.Start() + logging.OnError(err).Fatal("unable to start db") + + return config, func() { + logging.OnError(embedded.Stop()).Error("unable to stop db") + } +} + +// getPort returns a free port and locks it until close is called +func getPort() (port uint16, close func()) { + l, err := net.Listen("tcp", ":0") + logging.OnError(err).Fatal("unable to get port") + port = uint16(l.Addr().(*net.TCPAddr).Port) + logging.WithFields("port", port).Info("Port is available") + return port, func() { + logging.OnError(l.Close()).Error("unable to close port listener") + } +} diff --git a/internal/database/postgres/pg.go b/internal/database/postgres/pg.go index c847cc0a58..2f8bb29e17 100644 --- a/internal/database/postgres/pg.go +++ b/internal/database/postgres/pg.go @@ -18,7 +18,7 @@ import ( func init() { config := new(Config) - dialect.Register(config, config, false) + dialect.Register(config, config, true) } const ( @@ -29,16 +29,15 @@ const ( ) type Config struct { - Host string - Port int32 - Database string - EventPushConnRatio float64 - MaxOpenConns uint32 - MaxIdleConns uint32 - MaxConnLifetime time.Duration - MaxConnIdleTime time.Duration - User User - Admin AdminUser + Host string + Port int32 + Database string + MaxOpenConns uint32 + MaxIdleConns uint32 + MaxConnLifetime time.Duration + MaxConnIdleTime time.Duration + User User + Admin AdminUser // Additional options to be appended as options= // The value will be taken as is. Multiple options are space separated. Options string @@ -148,12 +147,8 @@ func (c *Config) Password() string { return c.User.Password } -func (c *Config) Type() string { - return "postgres" -} - -func (c *Config) Timetravel(time.Duration) string { - return "" +func (c *Config) Type() dialect.DatabaseType { + return dialect.DatabaseTypePostgres } type User struct { diff --git a/internal/domain/action.go b/internal/domain/action.go index 18dd23e8c5..b57dde6289 100644 --- a/internal/domain/action.go +++ b/internal/domain/action.go @@ -1,6 +1,7 @@ package domain import ( + "slices" "time" "github.com/zitadel/zitadel/internal/eventstore/v1/models" @@ -45,3 +46,51 @@ const ( ActionsMaxAllowed ActionsAllowedUnlimited ) + +type ActionFunction int32 + +const ( + ActionFunctionUnspecified ActionFunction = iota + ActionFunctionPreUserinfo + ActionFunctionPreAccessToken + ActionFunctionPreSAMLResponse + actionFunctionCount +) + +func (s ActionFunction) Valid() bool { + return s >= 0 && s < actionFunctionCount +} + +func (s ActionFunction) LocalizationKey() string { + if !s.Valid() { + return ActionFunctionUnspecified.LocalizationKey() + } + + switch s { + case ActionFunctionPreUserinfo: + return "preuserinfo" + case ActionFunctionPreAccessToken: + return "preaccesstoken" + case ActionFunctionPreSAMLResponse: + return "presamlresponse" + case ActionFunctionUnspecified, actionFunctionCount: + fallthrough + default: + return "unspecified" + } +} + +func AllActionFunctions() []string { + return []string{ + ActionFunctionPreUserinfo.LocalizationKey(), + ActionFunctionPreAccessToken.LocalizationKey(), + ActionFunctionPreSAMLResponse.LocalizationKey(), + } +} + +func ActionFunctionExists() func(string) bool { + functions := AllActionFunctions() + return func(s string) bool { + return slices.Contains(functions, s) + } +} diff --git a/internal/domain/flow.go b/internal/domain/flow.go index 143ce6bd0b..39cb13fc1e 100644 --- a/internal/domain/flow.go +++ b/internal/domain/flow.go @@ -1,7 +1,6 @@ package domain import ( - "slices" "strconv" ) @@ -150,20 +149,3 @@ func (s TriggerType) LocalizationKey() string { return "Action.TriggerType.Unspecified" } } - -func AllFunctions() []string { - functions := make([]string, 0) - for _, flowType := range AllFlowTypes() { - for _, triggerType := range flowType.TriggerTypes() { - functions = append(functions, flowType.LocalizationKey()+"."+triggerType.LocalizationKey()) - } - } - return functions -} - -func FunctionExists() func(string) bool { - functions := AllFunctions() - return func(s string) bool { - return slices.Contains(functions, s) - } -} diff --git a/internal/eventstore/eventstore_pusher_test.go b/internal/eventstore/eventstore_pusher_test.go index 4e8e663667..318cf1a37e 100644 --- a/internal/eventstore/eventstore_pusher_test.go +++ b/internal/eventstore/eventstore_pusher_test.go @@ -11,7 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" ) -func TestCRDB_Push_OneAggregate(t *testing.T) { +func TestEventstore_Push_OneAggregate(t *testing.T) { type args struct { ctx context.Context commands []eventstore.Command @@ -202,7 +202,7 @@ func TestCRDB_Push_OneAggregate(t *testing.T) { } } if _, err := db.Push(tt.args.ctx, tt.args.commands...); (err != nil) != tt.res.wantErr { - t.Errorf("CRDB.Push() error = %v, wantErr %v", err, tt.res.wantErr) + t.Errorf("eventstore.Push() error = %v, wantErr %v", err, tt.res.wantErr) } assertEventCount(t, @@ -218,7 +218,7 @@ func TestCRDB_Push_OneAggregate(t *testing.T) { } } -func TestCRDB_Push_MultipleAggregate(t *testing.T) { +func TestEventstore_Push_MultipleAggregate(t *testing.T) { type args struct { commands []eventstore.Command } @@ -312,7 +312,7 @@ func TestCRDB_Push_MultipleAggregate(t *testing.T) { }, ) if _, err := db.Push(context.Background(), tt.args.commands...); (err != nil) != tt.res.wantErr { - t.Errorf("CRDB.Push() error = %v, wantErr %v", err, tt.res.wantErr) + t.Errorf("eventstore.Push() error = %v, wantErr %v", err, tt.res.wantErr) } assertEventCount(t, clients[pusherName], tt.res.eventsRes.aggType, tt.res.eventsRes.aggID, tt.res.eventsRes.pushedEventsCount) @@ -321,7 +321,7 @@ func TestCRDB_Push_MultipleAggregate(t *testing.T) { } } -func TestCRDB_Push_Parallel(t *testing.T) { +func TestEventstore_Push_Parallel(t *testing.T) { type args struct { commands [][]eventstore.Command } @@ -453,7 +453,7 @@ func TestCRDB_Push_Parallel(t *testing.T) { } } -func TestCRDB_Push_ResourceOwner(t *testing.T) { +func TestEventstore_Push_ResourceOwner(t *testing.T) { type args struct { commands []eventstore.Command } @@ -587,7 +587,7 @@ func TestCRDB_Push_ResourceOwner(t *testing.T) { events, err := db.Push(context.Background(), tt.args.commands...) if err != nil { - t.Errorf("CRDB.Push() error = %v", err) + t.Errorf("eventstore.Push() error = %v", err) } if len(events) != len(tt.res.resourceOwners) { diff --git a/internal/eventstore/eventstore_querier_test.go b/internal/eventstore/eventstore_querier_test.go index 4b7ad78b25..3f23c5da75 100644 --- a/internal/eventstore/eventstore_querier_test.go +++ b/internal/eventstore/eventstore_querier_test.go @@ -7,7 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" ) -func TestCRDB_Filter(t *testing.T) { +func TestEventstore_Filter(t *testing.T) { type args struct { searchQuery *eventstore.SearchQueryBuilder } @@ -120,18 +120,18 @@ func TestCRDB_Filter(t *testing.T) { events, err := db.Filter(context.Background(), tt.args.searchQuery) if (err != nil) != tt.wantErr { - t.Errorf("CRDB.query() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("eventstore.query() error = %v, wantErr %v", err, tt.wantErr) } if len(events) != tt.res.eventCount { - t.Errorf("CRDB.query() expected event count: %d got %d", tt.res.eventCount, len(events)) + t.Errorf("eventstore.query() expected event count: %d got %d", tt.res.eventCount, len(events)) } }) } } } -func TestCRDB_LatestSequence(t *testing.T) { +func TestEventstore_LatestSequence(t *testing.T) { type args struct { searchQuery *eventstore.SearchQueryBuilder } @@ -204,10 +204,10 @@ func TestCRDB_LatestSequence(t *testing.T) { sequence, err := db.LatestSequence(context.Background(), tt.args.searchQuery) if (err != nil) != tt.wantErr { - t.Errorf("CRDB.query() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("eventstore.query() error = %v, wantErr %v", err, tt.wantErr) } if tt.res.sequence > sequence { - t.Errorf("CRDB.query() expected sequence: %v got %v", tt.res.sequence, sequence) + t.Errorf("eventstore.query() expected sequence: %v got %v", tt.res.sequence, sequence) } }) } diff --git a/internal/eventstore/example_test.go b/internal/eventstore/example_test.go index ee053d16da..2b6c205ddd 100644 --- a/internal/eventstore/example_test.go +++ b/internal/eventstore/example_test.go @@ -289,8 +289,8 @@ func (rm *UserReadModel) Reduce() error { func TestUserReadModel(t *testing.T) { es := eventstore.NewEventstore( &eventstore.Config{ - Querier: query_repo.NewCRDB(testCRDBClient), - Pusher: v3.NewEventstore(testCRDBClient), + Querier: query_repo.NewPostgres(testClient), + Pusher: v3.NewEventstore(testClient), }, ) diff --git a/internal/eventstore/handler/v2/handler.go b/internal/eventstore/handler/v2/handler.go index 5c1eeef95d..43c3e58b3b 100644 --- a/internal/eventstore/handler/v2/handler.go +++ b/internal/eventstore/handler/v2/handler.go @@ -665,7 +665,6 @@ func (h *Handler) eventQuery(currentState *state) *eventstore.SearchQueryBuilder builder := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). AwaitOpenTransactions(). Limit(uint64(h.bulkLimit)). - AllowTimeTravel(). OrderAsc(). InstanceID(currentState.instanceID) diff --git a/internal/eventstore/local_crdb_test.go b/internal/eventstore/local_postgres_test.go similarity index 63% rename from internal/eventstore/local_crdb_test.go rename to internal/eventstore/local_postgres_test.go index 87c5084fe7..d75292b3ff 100644 --- a/internal/eventstore/local_crdb_test.go +++ b/internal/eventstore/local_postgres_test.go @@ -8,111 +8,100 @@ import ( "testing" "time" - "github.com/cockroachdb/cockroach-go/v2/testserver" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/zitadel/logging" "github.com/zitadel/zitadel/cmd/initialise" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/database/cockroach" + "github.com/zitadel/zitadel/internal/database/dialect" + "github.com/zitadel/zitadel/internal/database/postgres" "github.com/zitadel/zitadel/internal/eventstore" es_sql "github.com/zitadel/zitadel/internal/eventstore/repository/sql" new_es "github.com/zitadel/zitadel/internal/eventstore/v3" ) var ( - testCRDBClient *database.DB - queriers map[string]eventstore.Querier = make(map[string]eventstore.Querier) - pushers map[string]eventstore.Pusher = make(map[string]eventstore.Pusher) - clients map[string]*database.DB = make(map[string]*database.DB) + testClient *database.DB + queriers map[string]eventstore.Querier = make(map[string]eventstore.Querier) + pushers map[string]eventstore.Pusher = make(map[string]eventstore.Pusher) + clients map[string]*database.DB = make(map[string]*database.DB) ) func TestMain(m *testing.M) { - opts := make([]testserver.TestServerOpt, 0, 1) - if version := os.Getenv("ZITADEL_CRDB_VERSION"); version != "" { - opts = append(opts, testserver.CustomVersionOpt(version)) - } - ts, err := testserver.NewTestServer(opts...) - if err != nil { - logging.WithFields("error", err).Fatal("unable to start db") - } + os.Exit(func() int { + config, cleanup := postgres.StartEmbedded() + defer cleanup() - testCRDBClient = &database.DB{ - Database: new(testDB), - } - - connConfig, err := pgxpool.ParseConfig(ts.PGURL().String()) - if err != nil { - logging.WithFields("error", err).Fatal("unable to parse db url") - } - connConfig.AfterConnect = new_es.RegisterEventstoreTypes - pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) - if err != nil { - logging.WithFields("error", err).Fatal("unable to create db pool") - } - testCRDBClient.DB = stdlib.OpenDBFromPool(pool) - if err = testCRDBClient.Ping(); err != nil { - logging.WithFields("error", err).Fatal("unable to ping db") - } - - v2 := &es_sql.CRDB{DB: testCRDBClient} - queriers["v2(inmemory)"] = v2 - clients["v2(inmemory)"] = testCRDBClient - - pushers["v3(inmemory)"] = new_es.NewEventstore(testCRDBClient) - clients["v3(inmemory)"] = testCRDBClient - - if localDB, err := connectLocalhost(); err == nil { - if err = initDB(context.Background(), localDB); err != nil { - logging.WithFields("error", err).Fatal("migrations failed") + testClient = &database.DB{ + Database: new(testDB), } - pushers["v3(singlenode)"] = new_es.NewEventstore(localDB) - clients["v3(singlenode)"] = localDB - } - // pushers["v2(inmemory)"] = v2 + connConfig, err := pgxpool.ParseConfig(config.GetConnectionURL()) + logging.OnError(err).Fatal("unable to parse db url") - defer func() { - testCRDBClient.Close() - ts.Stop() - }() + connConfig.AfterConnect = new_es.RegisterEventstoreTypes + pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) + logging.OnError(err).Fatal("unable to create db pool") - if err = initDB(context.Background(), testCRDBClient); err != nil { - logging.WithFields("error", err).Fatal("migrations failed") - } + testClient.DB = stdlib.OpenDBFromPool(pool) + err = testClient.Ping() + logging.OnError(err).Fatal("unable to ping db") - os.Exit(m.Run()) + v2 := &es_sql.Postgres{DB: testClient} + queriers["v2(inmemory)"] = v2 + clients["v2(inmemory)"] = testClient + + pushers["v3(inmemory)"] = new_es.NewEventstore(testClient) + clients["v3(inmemory)"] = testClient + + if localDB, err := connectLocalhost(); err == nil { + err = initDB(context.Background(), localDB) + logging.OnError(err).Fatal("migrations failed") + + pushers["v3(singlenode)"] = new_es.NewEventstore(localDB) + clients["v3(singlenode)"] = localDB + } + + defer func() { + logging.OnError(testClient.Close()).Error("unable to close db") + }() + + err = initDB(context.Background(), &database.DB{DB: testClient.DB, Database: &postgres.Config{Database: "zitadel"}}) + logging.OnError(err).Fatal("migrations failed") + + return m.Run() + }()) } func initDB(ctx context.Context, db *database.DB) error { - initialise.ReadStmts("cockroach") config := new(database.Config) - config.SetConnector(&cockroach.Config{ - User: cockroach.User{ - Username: "zitadel", - }, - Database: "zitadel", - }) + config.SetConnector(&postgres.Config{User: postgres.User{Username: "zitadel"}, Database: "zitadel"}) + + if err := initialise.ReadStmts(); err != nil { + return err + } + err := initialise.Init(ctx, db, initialise.VerifyUser(config.Username(), ""), initialise.VerifyDatabase(config.DatabaseName()), - initialise.VerifyGrant(config.DatabaseName(), config.Username()), - initialise.VerifySettings(config.DatabaseName(), config.Username())) + initialise.VerifyGrant(config.DatabaseName(), config.Username())) if err != nil { return err } + err = initialise.VerifyZitadel(ctx, db, *config) if err != nil { return err } + // create old events _, err = db.Exec(oldEventsTable) return err } func connectLocalhost() (*database.DB, error) { - client, err := sql.Open("pgx", "postgresql://root@localhost:26257/defaultdb?sslmode=disable") + client, err := sql.Open("pgx", "postgresql://postgres@localhost:5432/postgres?sslmode=disable") if err != nil { return nil, err } @@ -134,7 +123,7 @@ func (*testDB) DatabaseName() string { return "db" } func (*testDB) Username() string { return "user" } -func (*testDB) Type() string { return "cockroach" } +func (*testDB) Type() dialect.DatabaseType { return dialect.DatabaseTypePostgres } func generateCommand(aggregateType eventstore.AggregateType, aggregateID string, opts ...func(*testEvent)) eventstore.Command { e := &testEvent{ @@ -177,7 +166,7 @@ func canceledCtx() context.Context { } func fillUniqueData(unique_type, field, instanceID string) error { - _, err := testCRDBClient.Exec("INSERT INTO eventstore.unique_constraints (unique_type, unique_field, instance_id) VALUES ($1, $2, $3)", unique_type, field, instanceID) + _, err := testClient.Exec("INSERT INTO eventstore.unique_constraints (unique_type, unique_field, instance_id) VALUES ($1, $2, $3)", unique_type, field, instanceID) return err } @@ -251,5 +240,5 @@ const oldEventsTable = `CREATE TABLE IF NOT EXISTS eventstore.events ( , "position" DECIMAL NOT NULL , in_tx_order INTEGER NOT NULL - , PRIMARY KEY (instance_id, aggregate_type, aggregate_id, event_sequence DESC) + , PRIMARY KEY (instance_id, aggregate_type, aggregate_id, event_sequence) );` diff --git a/internal/eventstore/repository/search_query.go b/internal/eventstore/repository/search_query.go index f84c7f1201..6ffba31ca8 100644 --- a/internal/eventstore/repository/search_query.go +++ b/internal/eventstore/repository/search_query.go @@ -16,7 +16,6 @@ type SearchQuery struct { Tx *sql.Tx LockRows bool LockOption eventstore.LockOption - AllowTimeTravel bool AwaitOpenTransactions bool Limit uint64 Offset uint32 @@ -51,11 +50,11 @@ const ( OperationGreater // OperationLess compares if the given values is less than the stored one OperationLess - //OperationIn checks if a stored value matches one of the passed value list + // OperationIn checks if a stored value matches one of the passed value list OperationIn - //OperationJSONContains checks if a stored value matches the given json + // OperationJSONContains checks if a stored value matches the given json OperationJSONContains - //OperationNotIn checks if a stored value does not match one of the passed value list + // OperationNotIn checks if a stored value does not match one of the passed value list OperationNotIn operationCount @@ -65,25 +64,25 @@ const ( type Field int32 const ( - //FieldAggregateType represents the aggregate type field + // FieldAggregateType represents the aggregate type field FieldAggregateType Field = iota + 1 - //FieldAggregateID represents the aggregate id field + // FieldAggregateID represents the aggregate id field FieldAggregateID - //FieldSequence represents the sequence field + // FieldSequence represents the sequence field FieldSequence - //FieldResourceOwner represents the resource owner field + // FieldResourceOwner represents the resource owner field FieldResourceOwner - //FieldInstanceID represents the instance id field + // FieldInstanceID represents the instance id field FieldInstanceID - //FieldEditorService represents the editor service field + // FieldEditorService represents the editor service field FieldEditorService - //FieldEditorUser represents the editor user field + // FieldEditorUser represents the editor user field FieldEditorUser - //FieldEventType represents the event type field + // FieldEventType represents the event type field FieldEventType - //FieldEventData represents the event data field + // FieldEventData represents the event data field FieldEventData - //FieldCreationDate represents the creation date field + // FieldCreationDate represents the creation date field FieldCreationDate // FieldPosition represents the field of the global sequence FieldPosition @@ -129,7 +128,6 @@ func QueryFromBuilder(builder *eventstore.SearchQueryBuilder) (*SearchQuery, err Offset: builder.GetOffset(), Desc: builder.GetDesc(), Tx: builder.GetTx(), - AllowTimeTravel: builder.GetAllowTimeTravel(), AwaitOpenTransactions: builder.GetAwaitOpenTransactions(), SubQueries: make([][]*Filter, len(builder.GetQueries())), } diff --git a/internal/eventstore/repository/sql/crdb.go b/internal/eventstore/repository/sql/crdb.go deleted file mode 100644 index 68610676c3..0000000000 --- a/internal/eventstore/repository/sql/crdb.go +++ /dev/null @@ -1,455 +0,0 @@ -package sql - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "regexp" - "strconv" - "strings" - - "github.com/cockroachdb/cockroach-go/v2/crdb" - "github.com/jackc/pgx/v5/pgconn" - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/repository" - "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/zerrors" -) - -const ( - //as soon as stored procedures are possible in crdb - // we could move the code to migrations and call the procedure - // traking issue: https://github.com/cockroachdb/cockroach/issues/17511 - // - //previous_data selects the needed data of the latest event of the aggregate - // and buffers it (crdb inmemory) - crdbInsert = "WITH previous_data (aggregate_type_sequence, aggregate_sequence, resource_owner) AS (" + - "SELECT agg_type.seq, agg.seq, agg.ro FROM " + - "(" + - //max sequence of requested aggregate type - " SELECT MAX(event_sequence) seq, 1 join_me" + - " FROM eventstore.events" + - " WHERE aggregate_type = $2" + - " AND (CASE WHEN $9::TEXT IS NULL THEN instance_id is null else instance_id = $9::TEXT END)" + - ") AS agg_type " + - // combined with - "LEFT JOIN " + - "(" + - // max sequence and resource owner of aggregate root - " SELECT event_sequence seq, resource_owner ro, 1 join_me" + - " FROM eventstore.events" + - " WHERE aggregate_type = $2 AND aggregate_id = $3" + - " AND (CASE WHEN $9::TEXT IS NULL THEN instance_id is null else instance_id = $9::TEXT END)" + - " ORDER BY event_sequence DESC" + - " LIMIT 1" + - ") AS agg USING(join_me)" + - ") " + - "INSERT INTO eventstore.events (" + - " event_type," + - " aggregate_type," + - " aggregate_id," + - " aggregate_version," + - " creation_date," + - " position," + - " event_data," + - " editor_user," + - " editor_service," + - " resource_owner," + - " instance_id," + - " event_sequence," + - " previous_aggregate_sequence," + - " previous_aggregate_type_sequence," + - " in_tx_order" + - ") " + - // defines the data to be inserted - "SELECT" + - " $1::VARCHAR AS event_type," + - " $2::VARCHAR AS aggregate_type," + - " $3::VARCHAR AS aggregate_id," + - " $4::VARCHAR AS aggregate_version," + - " hlc_to_timestamp(cluster_logical_timestamp()) AS creation_date," + - " cluster_logical_timestamp() AS position," + - " $5::JSONB AS event_data," + - " $6::VARCHAR AS editor_user," + - " $7::VARCHAR AS editor_service," + - " COALESCE((resource_owner), $8::VARCHAR) AS resource_owner," + - " $9::VARCHAR AS instance_id," + - " COALESCE(aggregate_sequence, 0)+1," + - " aggregate_sequence AS previous_aggregate_sequence," + - " aggregate_type_sequence AS previous_aggregate_type_sequence," + - " $10 AS in_tx_order " + - "FROM previous_data " + - "RETURNING id, event_sequence, creation_date, resource_owner, instance_id" - - uniqueInsert = `INSERT INTO eventstore.unique_constraints - ( - unique_type, - unique_field, - instance_id - ) - VALUES ( - $1, - $2, - $3 - )` - - uniqueDelete = `DELETE FROM eventstore.unique_constraints - WHERE unique_type = $1 and unique_field = $2 and instance_id = $3` - uniqueDeleteInstance = `DELETE FROM eventstore.unique_constraints - WHERE instance_id = $1` -) - -// awaitOpenTransactions ensures event ordering, so we don't events younger that open transactions -var ( - awaitOpenTransactionsV1 string - awaitOpenTransactionsV2 string -) - -func awaitOpenTransactions(useV1 bool) string { - if useV1 { - return awaitOpenTransactionsV1 - } - return awaitOpenTransactionsV2 -} - -type CRDB struct { - *database.DB -} - -func NewCRDB(client *database.DB) *CRDB { - switch client.Type() { - case "cockroach": - awaitOpenTransactionsV1 = " AND creation_date::TIMESTAMP < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))" - awaitOpenTransactionsV2 = ` AND hlc_to_timestamp("position") < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))` - case "postgres": - awaitOpenTransactionsV1 = ` AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')` - awaitOpenTransactionsV2 = ` AND "position" < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')` - } - - return &CRDB{client} -} - -func (db *CRDB) Health(ctx context.Context) error { return db.Ping() } - -// Push adds all events to the eventstreams of the aggregates. -// This call is transaction save. The transaction will be rolled back if one event fails -func (db *CRDB) Push(ctx context.Context, commands ...eventstore.Command) (events []eventstore.Event, err error) { - events = make([]eventstore.Event, len(commands)) - - err = crdb.ExecuteTx(ctx, db.DB.DB, nil, func(tx *sql.Tx) error { - - var uniqueConstraints []*eventstore.UniqueConstraint - - for i, command := range commands { - if command.Aggregate().InstanceID == "" { - command.Aggregate().InstanceID = authz.GetInstance(ctx).InstanceID() - } - - var payload []byte - if command.Payload() != nil { - payload, err = json.Marshal(command.Payload()) - if err != nil { - return err - } - } - e := &repository.Event{ - Typ: command.Type(), - Data: payload, - EditorUser: command.Creator(), - Version: command.Aggregate().Version, - AggregateID: command.Aggregate().ID, - AggregateType: command.Aggregate().Type, - ResourceOwner: sql.NullString{String: command.Aggregate().ResourceOwner, Valid: command.Aggregate().ResourceOwner != ""}, - InstanceID: command.Aggregate().InstanceID, - } - - err := tx.QueryRowContext(ctx, crdbInsert, - e.Type(), - e.Aggregate().Type, - e.Aggregate().ID, - e.Aggregate().Version, - payload, - e.Creator(), - "zitadel", - e.Aggregate().ResourceOwner, - e.Aggregate().InstanceID, - i, - ).Scan(&e.ID, &e.Seq, &e.CreationDate, &e.ResourceOwner, &e.InstanceID) - - if err != nil { - logging.WithFields( - "aggregate", e.Aggregate().Type, - "aggregateId", e.Aggregate().ID, - "aggregateType", e.Aggregate().Type, - "eventType", e.Type(), - "instanceID", e.Aggregate().InstanceID, - ).WithError(err).Debug("query failed") - return zerrors.ThrowInternal(err, "SQL-SBP37", "unable to create event") - } - - uniqueConstraints = append(uniqueConstraints, command.UniqueConstraints()...) - events[i] = e - } - - return db.handleUniqueConstraints(ctx, tx, uniqueConstraints...) - }) - if err != nil && !errors.Is(err, &zerrors.ZitadelError{}) { - err = zerrors.ThrowInternal(err, "SQL-DjgtG", "unable to store events") - } - - return events, err -} - -// handleUniqueConstraints adds or removes unique constraints -func (db *CRDB) handleUniqueConstraints(ctx context.Context, tx *sql.Tx, uniqueConstraints ...*eventstore.UniqueConstraint) (err error) { - if len(uniqueConstraints) == 0 || (len(uniqueConstraints) == 1 && uniqueConstraints[0] == nil) { - return nil - } - - for _, uniqueConstraint := range uniqueConstraints { - uniqueConstraint.UniqueField = strings.ToLower(uniqueConstraint.UniqueField) - switch uniqueConstraint.Action { - case eventstore.UniqueConstraintAdd: - _, err := tx.ExecContext(ctx, uniqueInsert, uniqueConstraint.UniqueType, uniqueConstraint.UniqueField, authz.GetInstance(ctx).InstanceID()) - if err != nil { - logging.WithFields( - "unique_type", uniqueConstraint.UniqueType, - "unique_field", uniqueConstraint.UniqueField).WithError(err).Info("insert unique constraint failed") - - if db.isUniqueViolationError(err) { - return zerrors.ThrowAlreadyExists(err, "SQL-wHcEq", uniqueConstraint.ErrorMessage) - } - - return zerrors.ThrowInternal(err, "SQL-dM9ds", "unable to create unique constraint") - } - case eventstore.UniqueConstraintRemove: - _, err := tx.ExecContext(ctx, uniqueDelete, uniqueConstraint.UniqueType, uniqueConstraint.UniqueField, authz.GetInstance(ctx).InstanceID()) - if err != nil { - logging.WithFields( - "unique_type", uniqueConstraint.UniqueType, - "unique_field", uniqueConstraint.UniqueField).WithError(err).Info("delete unique constraint failed") - return zerrors.ThrowInternal(err, "SQL-6n88i", "unable to remove unique constraint") - } - case eventstore.UniqueConstraintInstanceRemove: - _, err := tx.ExecContext(ctx, uniqueDeleteInstance, authz.GetInstance(ctx).InstanceID()) - if err != nil { - logging.WithFields( - "instance_id", authz.GetInstance(ctx).InstanceID()).WithError(err).Info("delete instance unique constraints failed") - return zerrors.ThrowInternal(err, "SQL-6n88i", "unable to remove unique constraints of instance") - } - } - } - return nil -} - -// FilterToReducer finds all events matching the given search query and passes them to the reduce function. -func (crdb *CRDB) FilterToReducer(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder, reduce eventstore.Reducer) (err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - err = query(ctx, crdb, searchQuery, reduce, false) - if err == nil { - return nil - } - pgErr := new(pgconn.PgError) - // check events2 not exists - if errors.As(err, &pgErr) && pgErr.Code == "42P01" { - return query(ctx, crdb, searchQuery, reduce, true) - } - return err -} - -// LatestSequence returns the latest sequence found by the search query -func (db *CRDB) LatestSequence(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) (float64, error) { - var position sql.NullFloat64 - err := query(ctx, db, searchQuery, &position, false) - return position.Float64, err -} - -// InstanceIDs returns the instance ids found by the search query -func (db *CRDB) InstanceIDs(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) ([]string, error) { - var ids []string - err := query(ctx, db, searchQuery, &ids, false) - if err != nil { - return nil, err - } - return ids, nil -} - -func (db *CRDB) Client() *database.DB { - return db.DB -} - -func (db *CRDB) orderByEventSequence(desc, shouldOrderBySequence, useV1 bool) string { - if useV1 { - if desc { - return ` ORDER BY event_sequence DESC` - } - return ` ORDER BY event_sequence` - } - if shouldOrderBySequence { - if desc { - return ` ORDER BY "sequence" DESC` - } - return ` ORDER BY "sequence"` - } - - if desc { - return ` ORDER BY "position" DESC, in_tx_order DESC` - } - return ` ORDER BY "position", in_tx_order` -} - -func (db *CRDB) eventQuery(useV1 bool) string { - if useV1 { - return "SELECT" + - " creation_date" + - ", event_type" + - ", event_sequence" + - ", event_data" + - ", editor_user" + - ", resource_owner" + - ", instance_id" + - ", aggregate_type" + - ", aggregate_id" + - ", aggregate_version" + - " FROM eventstore.events" - } - return "SELECT" + - " created_at" + - ", event_type" + - `, "sequence"` + - `, "position"` + - ", payload" + - ", creator" + - `, "owner"` + - ", instance_id" + - ", aggregate_type" + - ", aggregate_id" + - ", revision" + - " FROM eventstore.events2" -} - -func (db *CRDB) maxSequenceQuery(useV1 bool) string { - if useV1 { - return `SELECT event_sequence FROM eventstore.events` - } - return `SELECT "position" FROM eventstore.events2` -} - -func (db *CRDB) instanceIDsQuery(useV1 bool) string { - table := "eventstore.events2" - if useV1 { - table = "eventstore.events" - } - return "SELECT DISTINCT instance_id FROM " + table -} - -func (db *CRDB) columnName(col repository.Field, useV1 bool) string { - switch col { - case repository.FieldAggregateID: - return "aggregate_id" - case repository.FieldAggregateType: - return "aggregate_type" - case repository.FieldSequence: - if useV1 { - return "event_sequence" - } - return `"sequence"` - case repository.FieldResourceOwner: - if useV1 { - return "resource_owner" - } - return `"owner"` - case repository.FieldInstanceID: - return "instance_id" - case repository.FieldEditorService: - if useV1 { - return "editor_service" - } - return "" - case repository.FieldEditorUser: - if useV1 { - return "editor_user" - } - return "creator" - case repository.FieldEventType: - return "event_type" - case repository.FieldEventData: - if useV1 { - return "event_data" - } - return "payload" - case repository.FieldCreationDate: - if useV1 { - return "creation_date" - } - return "created_at" - case repository.FieldPosition: - return `"position"` - default: - return "" - } -} - -func (db *CRDB) conditionFormat(operation repository.Operation) string { - switch operation { - case repository.OperationIn: - return "%s %s ANY(?)" - case repository.OperationNotIn: - return "%s %s ALL(?)" - } - return "%s %s ?" -} - -func (db *CRDB) operation(operation repository.Operation) string { - switch operation { - case repository.OperationEquals, repository.OperationIn: - return "=" - case repository.OperationGreater: - return ">" - case repository.OperationLess: - return "<" - case repository.OperationJSONContains: - return "@>" - case repository.OperationNotIn: - return "<>" - } - return "" -} - -var ( - placeholder = regexp.MustCompile(`\?`) -) - -// placeholder replaces all "?" with postgres placeholders ($) -func (db *CRDB) placeholder(query string) string { - occurances := placeholder.FindAllStringIndex(query, -1) - if len(occurances) == 0 { - return query - } - replaced := query[:occurances[0][0]] - - for i, l := range occurances { - nextIDX := len(query) - if i < len(occurances)-1 { - nextIDX = occurances[i+1][0] - } - replaced = replaced + "$" + strconv.Itoa(i+1) + query[l[1]:nextIDX] - } - return replaced -} - -func (db *CRDB) isUniqueViolationError(err error) bool { - if pgxErr, ok := err.(*pgconn.PgError); ok { - if pgxErr.Code == "23505" { - return true - } - } - return false -} diff --git a/internal/eventstore/repository/sql/local_crdb_test.go b/internal/eventstore/repository/sql/local_postgres_test.go similarity index 54% rename from internal/eventstore/repository/sql/local_crdb_test.go rename to internal/eventstore/repository/sql/local_postgres_test.go index 0f8c934b47..765da213e3 100644 --- a/internal/eventstore/repository/sql/local_crdb_test.go +++ b/internal/eventstore/repository/sql/local_postgres_test.go @@ -7,72 +7,61 @@ import ( "testing" "time" - "github.com/cockroachdb/cockroach-go/v2/testserver" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/zitadel/logging" "github.com/zitadel/zitadel/cmd/initialise" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/database/cockroach" + "github.com/zitadel/zitadel/internal/database/dialect" + "github.com/zitadel/zitadel/internal/database/postgres" new_es "github.com/zitadel/zitadel/internal/eventstore/v3" ) var ( - testCRDBClient *sql.DB + testClient *sql.DB ) func TestMain(m *testing.M) { - opts := make([]testserver.TestServerOpt, 0, 1) - if version := os.Getenv("ZITADEL_CRDB_VERSION"); version != "" { - opts = append(opts, testserver.CustomVersionOpt(version)) - } - ts, err := testserver.NewTestServer(opts...) - if err != nil { - logging.WithFields("error", err).Fatal("unable to start db") - } + os.Exit(func() int { + config, cleanup := postgres.StartEmbedded() + defer cleanup() - connConfig, err := pgxpool.ParseConfig(ts.PGURL().String()) - if err != nil { - logging.WithFields("error", err).Fatal("unable to parse db url") - } - connConfig.AfterConnect = new_es.RegisterEventstoreTypes - pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) - if err != nil { - logging.WithFields("error", err).Fatal("unable to create db pool") - } + connConfig, err := pgxpool.ParseConfig(config.GetConnectionURL()) + logging.OnError(err).Fatal("unable to parse db url") - testCRDBClient = stdlib.OpenDBFromPool(pool) + connConfig.AfterConnect = new_es.RegisterEventstoreTypes + pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) + logging.OnError(err).Fatal("unable to create db pool") - if err = testCRDBClient.Ping(); err != nil { - logging.WithFields("error", err).Fatal("unable to ping db") - } + testClient = stdlib.OpenDBFromPool(pool) - defer func() { - testCRDBClient.Close() - ts.Stop() - }() + err = testClient.Ping() + logging.OnError(err).Fatal("unable to ping db") - if err = initDB(context.Background(), &database.DB{DB: testCRDBClient, Database: &cockroach.Config{Database: "zitadel"}}); err != nil { - logging.WithFields("error", err).Fatal("migrations failed") - } + defer func() { + logging.OnError(testClient.Close()).Error("unable to close db") + }() - os.Exit(m.Run()) + err = initDB(context.Background(), &database.DB{DB: testClient, Database: &postgres.Config{Database: "zitadel"}}) + logging.OnError(err).Fatal("migrations failed") + + return m.Run() + }()) } func initDB(ctx context.Context, db *database.DB) error { config := new(database.Config) - config.SetConnector(&cockroach.Config{User: cockroach.User{Username: "zitadel"}, Database: "zitadel"}) + config.SetConnector(&postgres.Config{User: postgres.User{Username: "zitadel"}, Database: "zitadel"}) - if err := initialise.ReadStmts("cockroach"); err != nil { + if err := initialise.ReadStmts(); err != nil { return err } err := initialise.Init(ctx, db, initialise.VerifyUser(config.Username(), ""), initialise.VerifyDatabase(config.DatabaseName()), - initialise.VerifyGrant(config.DatabaseName(), config.Username()), - initialise.VerifySettings(config.DatabaseName(), config.Username())) + initialise.VerifyGrant(config.DatabaseName(), config.Username())) if err != nil { return err } @@ -95,7 +84,7 @@ func (*testDB) DatabaseName() string { return "db" } func (*testDB) Username() string { return "user" } -func (*testDB) Type() string { return "cockroach" } +func (*testDB) Type() dialect.DatabaseType { return dialect.DatabaseTypePostgres } const oldEventsTable = `CREATE TABLE IF NOT EXISTS eventstore.events ( id UUID DEFAULT gen_random_uuid() @@ -116,5 +105,5 @@ const oldEventsTable = `CREATE TABLE IF NOT EXISTS eventstore.events ( , "position" DECIMAL NOT NULL , in_tx_order INTEGER NOT NULL - , PRIMARY KEY (instance_id, aggregate_type, aggregate_id, event_sequence DESC) + , PRIMARY KEY (instance_id, aggregate_type, aggregate_id, event_sequence) );` diff --git a/internal/eventstore/repository/sql/postgres.go b/internal/eventstore/repository/sql/postgres.go new file mode 100644 index 0000000000..bc9ad2e029 --- /dev/null +++ b/internal/eventstore/repository/sql/postgres.go @@ -0,0 +1,240 @@ +package sql + +import ( + "context" + "database/sql" + "errors" + "regexp" + "strconv" + + "github.com/jackc/pgx/v5/pgconn" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +// awaitOpenTransactions ensures event ordering, so we don't events younger that open transactions +var ( + awaitOpenTransactionsV1 = ` AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')` + awaitOpenTransactionsV2 = ` AND "position" < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')` +) + +func awaitOpenTransactions(useV1 bool) string { + if useV1 { + return awaitOpenTransactionsV1 + } + return awaitOpenTransactionsV2 +} + +type Postgres struct { + *database.DB +} + +func NewPostgres(client *database.DB) *Postgres { + return &Postgres{client} +} + +func (db *Postgres) Health(ctx context.Context) error { return db.Ping() } + +// FilterToReducer finds all events matching the given search query and passes them to the reduce function. +func (psql *Postgres) FilterToReducer(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder, reduce eventstore.Reducer) (err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + err = query(ctx, psql, searchQuery, reduce, false) + if err == nil { + return nil + } + pgErr := new(pgconn.PgError) + // check events2 not exists + if errors.As(err, &pgErr) && pgErr.Code == "42P01" { + return query(ctx, psql, searchQuery, reduce, true) + } + return err +} + +// LatestSequence returns the latest sequence found by the search query +func (db *Postgres) LatestSequence(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) (float64, error) { + var position sql.NullFloat64 + err := query(ctx, db, searchQuery, &position, false) + return position.Float64, err +} + +// InstanceIDs returns the instance ids found by the search query +func (db *Postgres) InstanceIDs(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) ([]string, error) { + var ids []string + err := query(ctx, db, searchQuery, &ids, false) + if err != nil { + return nil, err + } + return ids, nil +} + +func (db *Postgres) Client() *database.DB { + return db.DB +} + +func (db *Postgres) orderByEventSequence(desc, shouldOrderBySequence, useV1 bool) string { + if useV1 { + if desc { + return ` ORDER BY event_sequence DESC` + } + return ` ORDER BY event_sequence` + } + if shouldOrderBySequence { + if desc { + return ` ORDER BY "sequence" DESC` + } + return ` ORDER BY "sequence"` + } + + if desc { + return ` ORDER BY "position" DESC, in_tx_order DESC` + } + return ` ORDER BY "position", in_tx_order` +} + +func (db *Postgres) eventQuery(useV1 bool) string { + if useV1 { + return "SELECT" + + " creation_date" + + ", event_type" + + ", event_sequence" + + ", event_data" + + ", editor_user" + + ", resource_owner" + + ", instance_id" + + ", aggregate_type" + + ", aggregate_id" + + ", aggregate_version" + + " FROM eventstore.events" + } + return "SELECT" + + " created_at" + + ", event_type" + + `, "sequence"` + + `, "position"` + + ", payload" + + ", creator" + + `, "owner"` + + ", instance_id" + + ", aggregate_type" + + ", aggregate_id" + + ", revision" + + " FROM eventstore.events2" +} + +func (db *Postgres) maxSequenceQuery(useV1 bool) string { + if useV1 { + return `SELECT event_sequence FROM eventstore.events` + } + return `SELECT "position" FROM eventstore.events2` +} + +func (db *Postgres) instanceIDsQuery(useV1 bool) string { + table := "eventstore.events2" + if useV1 { + table = "eventstore.events" + } + return "SELECT DISTINCT instance_id FROM " + table +} + +func (db *Postgres) columnName(col repository.Field, useV1 bool) string { + switch col { + case repository.FieldAggregateID: + return "aggregate_id" + case repository.FieldAggregateType: + return "aggregate_type" + case repository.FieldSequence: + if useV1 { + return "event_sequence" + } + return `"sequence"` + case repository.FieldResourceOwner: + if useV1 { + return "resource_owner" + } + return `"owner"` + case repository.FieldInstanceID: + return "instance_id" + case repository.FieldEditorService: + if useV1 { + return "editor_service" + } + return "" + case repository.FieldEditorUser: + if useV1 { + return "editor_user" + } + return "creator" + case repository.FieldEventType: + return "event_type" + case repository.FieldEventData: + if useV1 { + return "event_data" + } + return "payload" + case repository.FieldCreationDate: + if useV1 { + return "creation_date" + } + return "created_at" + case repository.FieldPosition: + return `"position"` + default: + return "" + } +} + +func (db *Postgres) conditionFormat(operation repository.Operation) string { + switch operation { + case repository.OperationIn: + return "%s %s ANY(?)" + case repository.OperationNotIn: + return "%s %s ALL(?)" + case repository.OperationEquals, repository.OperationGreater, repository.OperationLess, repository.OperationJSONContains: + fallthrough + default: + return "%s %s ?" + } +} + +func (db *Postgres) operation(operation repository.Operation) string { + switch operation { + case repository.OperationEquals, repository.OperationIn: + return "=" + case repository.OperationGreater: + return ">" + case repository.OperationLess: + return "<" + case repository.OperationJSONContains: + return "@>" + case repository.OperationNotIn: + return "<>" + } + return "" +} + +var ( + placeholder = regexp.MustCompile(`\?`) +) + +// placeholder replaces all "?" with postgres placeholders ($) +func (db *Postgres) placeholder(query string) string { + occurrences := placeholder.FindAllStringIndex(query, -1) + if len(occurrences) == 0 { + return query + } + replaced := query[:occurrences[0][0]] + + for i, l := range occurrences { + nextIDX := len(query) + if i < len(occurrences)-1 { + nextIDX = occurrences[i+1][0] + } + replaced = replaced + "$" + strconv.Itoa(i+1) + query[l[1]:nextIDX] + } + return replaced +} diff --git a/internal/eventstore/repository/sql/crdb_test.go b/internal/eventstore/repository/sql/postgres_test.go similarity index 90% rename from internal/eventstore/repository/sql/crdb_test.go rename to internal/eventstore/repository/sql/postgres_test.go index a3f3331a82..151fdd1b6a 100644 --- a/internal/eventstore/repository/sql/crdb_test.go +++ b/internal/eventstore/repository/sql/postgres_test.go @@ -8,7 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/repository" ) -func TestCRDB_placeholder(t *testing.T) { +func TestPostgres_placeholder(t *testing.T) { type args struct { query string } @@ -50,15 +50,15 @@ func TestCRDB_placeholder(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := &CRDB{} + db := &Postgres{} if query := db.placeholder(tt.args.query); query != tt.res.query { - t.Errorf("CRDB.placeholder() = %v, want %v", query, tt.res.query) + t.Errorf("Postgres.placeholder() = %v, want %v", query, tt.res.query) } }) } } -func TestCRDB_operation(t *testing.T) { +func TestPostgres_operation(t *testing.T) { type res struct { op string } @@ -118,15 +118,15 @@ func TestCRDB_operation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := &CRDB{} + db := &Postgres{} if got := db.operation(tt.args.operation); got != tt.res.op { - t.Errorf("CRDB.operation() = %v, want %v", got, tt.res.op) + t.Errorf("Postgres.operation() = %v, want %v", got, tt.res.op) } }) } } -func TestCRDB_conditionFormat(t *testing.T) { +func TestPostgres_conditionFormat(t *testing.T) { type res struct { format string } @@ -159,15 +159,15 @@ func TestCRDB_conditionFormat(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := &CRDB{} + db := &Postgres{} if got := db.conditionFormat(tt.args.operation); got != tt.res.format { - t.Errorf("CRDB.conditionFormat() = %v, want %v", got, tt.res.format) + t.Errorf("Postgres.conditionFormat() = %v, want %v", got, tt.res.format) } }) } } -func TestCRDB_columnName(t *testing.T) { +func TestPostgres_columnName(t *testing.T) { type res struct { name string } @@ -295,9 +295,9 @@ func TestCRDB_columnName(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := &CRDB{} + db := &Postgres{} if got := db.columnName(tt.args.field, tt.args.useV1); got != tt.res.name { - t.Errorf("CRDB.operation() = %v, want %v", got, tt.res.name) + t.Errorf("Postgres.operation() = %v, want %v", got, tt.res.name) } }) } diff --git a/internal/eventstore/repository/sql/query.go b/internal/eventstore/repository/sql/query.go index 4e1cc87aff..a545225d9e 100644 --- a/internal/eventstore/repository/sql/query.go +++ b/internal/eventstore/repository/sql/query.go @@ -11,7 +11,6 @@ import ( "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database/dialect" "github.com/zitadel/zitadel/internal/eventstore" @@ -65,11 +64,6 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search if where == "" || query == "" { return zerrors.ThrowInvalidArgument(nil, "SQL-rWeBw", "invalid query factory") } - if q.Tx == nil { - if travel := prepareTimeTravel(ctx, criteria, q.AllowTimeTravel); travel != "" { - query += travel - } - } query += where // instead of using the max function of the database (which doesn't work for postgres) @@ -158,15 +152,7 @@ func prepareColumns(criteria querier, columns eventstore.Columns, useV1 bool) (s } } -func prepareTimeTravel(ctx context.Context, criteria querier, allow bool) string { - if !allow { - return "" - } - took := call.Took(ctx) - return criteria.Timetravel(took) -} - -func maxSequenceScanner(row scan, dest interface{}) (err error) { +func maxSequenceScanner(row scan, dest any) (err error) { position, ok := dest.(*sql.NullFloat64) if !ok { return zerrors.ThrowInvalidArgumentf(nil, "SQL-NBjA9", "type must be sql.NullInt64 got: %T", dest) diff --git a/internal/eventstore/repository/sql/query_test.go b/internal/eventstore/repository/sql/query_test.go index abac19ead0..3df819be64 100644 --- a/internal/eventstore/repository/sql/query_test.go +++ b/internal/eventstore/repository/sql/query_test.go @@ -14,10 +14,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/database/cockroach" db_mock "github.com/zitadel/zitadel/internal/database/mock" + "github.com/zitadel/zitadel/internal/database/postgres" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" + new_es "github.com/zitadel/zitadel/internal/eventstore/v3" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -68,7 +69,7 @@ func Test_getCondition(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := &CRDB{} + db := &Postgres{} if got := getCondition(db, tt.args.filter, false); got != tt.want { t.Errorf("getCondition() = %v, want %v", got, tt.want) } @@ -236,8 +237,7 @@ func Test_prepareColumns(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - crdb := &CRDB{} - query, rowScanner := prepareColumns(crdb, tt.args.columns, tt.args.useV1) + query, rowScanner := prepareColumns(new(Postgres), tt.args.columns, tt.args.useV1) if query != tt.res.query { t.Errorf("prepareColumns() got = %s, want %s", query, tt.res.query) } @@ -267,7 +267,7 @@ func Test_prepareColumns(t *testing.T) { got := reflect.Indirect(reflect.ValueOf(tt.args.dest)).Interface() if !reflect.DeepEqual(got, tt.res.expected) { - t.Errorf("unexpected result from rowScanner \nwant: %+v \ngot: %+v", tt.res.expected, got) + t.Errorf("unexpected result from rowScanner nwant: %+v ngot: %+v", tt.res.expected, got) } }) } @@ -403,7 +403,7 @@ func Test_prepareCondition(t *testing.T) { useV1: true, }, res: res{ - clause: " WHERE aggregate_type = ANY(?) AND creation_date::TIMESTAMP < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))", + clause: " WHERE aggregate_type = ANY(?) AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')", values: []interface{}{[]eventstore.AggregateType{"user", "org"}, database.TextArray[string]{}}, }, }, @@ -420,7 +420,7 @@ func Test_prepareCondition(t *testing.T) { }, }, res: res{ - clause: ` WHERE aggregate_type = ANY(?) AND hlc_to_timestamp("position") < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))`, + clause: ` WHERE aggregate_type = ANY(?) AND "position" < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')`, values: []interface{}{[]eventstore.AggregateType{"user", "org"}, database.TextArray[string]{}}, }, }, @@ -440,7 +440,7 @@ func Test_prepareCondition(t *testing.T) { useV1: true, }, res: res{ - clause: " WHERE aggregate_type = ANY(?) AND aggregate_id = ? AND event_type = ANY(?) AND creation_date::TIMESTAMP < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))", + clause: " WHERE aggregate_type = ANY(?) AND aggregate_id = ? AND event_type = ANY(?) AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')", values: []interface{}{[]eventstore.AggregateType{"user", "org"}, "1234", []eventstore.EventType{"user.created", "org.created"}, database.TextArray[string]{}}, }, }, @@ -459,15 +459,14 @@ func Test_prepareCondition(t *testing.T) { }, }, res: res{ - clause: ` WHERE aggregate_type = ANY(?) AND aggregate_id = ? AND event_type = ANY(?) AND hlc_to_timestamp("position") < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))`, + clause: ` WHERE aggregate_type = ANY(?) AND aggregate_id = ? AND event_type = ANY(?) AND "position" < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')`, values: []interface{}{[]eventstore.AggregateType{"user", "org"}, "1234", []eventstore.EventType{"user.created", "org.created"}, database.TextArray[string]{}}, }, }, } - crdb := NewCRDB(&database.DB{Database: new(cockroach.Config)}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotClause, gotValues := prepareConditions(crdb, tt.args.query, tt.args.useV1) + gotClause, gotValues := prepareConditions(NewPostgres(&database.DB{Database: new(postgres.Config)}), tt.args.query, tt.args.useV1) if gotClause != tt.res.clause { t.Errorf("prepareCondition() gotClause = %v, want %v", gotClause, tt.res.clause) } @@ -484,7 +483,7 @@ func Test_prepareCondition(t *testing.T) { } } -func Test_query_events_with_crdb(t *testing.T) { +func Test_query_events_with_postgres(t *testing.T) { type args struct { searchQuery *eventstore.SearchQueryBuilder } @@ -511,7 +510,7 @@ func Test_query_events_with_crdb(t *testing.T) { Builder(), }, fields: fields{ - client: testCRDBClient, + client: testClient, existingEvents: []eventstore.Command{ generateEvent(t, "300"), generateEvent(t, "300"), @@ -532,7 +531,7 @@ func Test_query_events_with_crdb(t *testing.T) { Builder(), }, fields: fields{ - client: testCRDBClient, + client: testClient, existingEvents: []eventstore.Command{ generateEvent(t, "301"), generateEvent(t, "302"), @@ -555,7 +554,7 @@ func Test_query_events_with_crdb(t *testing.T) { Builder(), }, fields: fields{ - client: testCRDBClient, + client: testClient, existingEvents: []eventstore.Command{ generateEvent(t, "303"), generateEvent(t, "303"), @@ -576,7 +575,7 @@ func Test_query_events_with_crdb(t *testing.T) { ResourceOwner("caos"), }, fields: fields{ - client: testCRDBClient, + client: testClient, existingEvents: []eventstore.Command{ generateEvent(t, "306", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), generateEvent(t, "307", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), @@ -599,7 +598,7 @@ func Test_query_events_with_crdb(t *testing.T) { Builder(), }, fields: fields{ - client: testCRDBClient, + client: testClient, existingEvents: []eventstore.Command{ generateEvent(t, "311", func(e *repository.Event) { e.Typ = "user.created" }), generateEvent(t, "311", func(e *repository.Event) { e.Typ = "user.updated" }), @@ -623,7 +622,7 @@ func Test_query_events_with_crdb(t *testing.T) { searchQuery: eventstore.NewSearchQueryBuilder(eventstore.Columns(-1)), }, fields: fields{ - client: testCRDBClient, + client: testClient, existingEvents: []eventstore.Command{}, }, res: res{ @@ -634,117 +633,37 @@ func Test_query_events_with_crdb(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := &CRDB{ - DB: &database.DB{ - DB: tt.fields.client, - Database: new(testDB), - }, + dbClient := &database.DB{ + DB: tt.fields.client, + Database: new(testDB), } + client := &Postgres{ + DB: dbClient, + } + + pusher := new_es.NewEventstore(dbClient) // setup initial data for query - if _, err := db.Push(context.Background(), tt.fields.existingEvents...); err != nil { + if _, err := pusher.Push(context.Background(), dbClient.DB, tt.fields.existingEvents...); err != nil { t.Errorf("error in setup = %v", err) return } events := []eventstore.Event{} - if err := query(context.Background(), db, tt.args.searchQuery, eventstore.Reducer(func(event eventstore.Event) error { + if err := query(context.Background(), client, tt.args.searchQuery, eventstore.Reducer(func(event eventstore.Event) error { events = append(events, event) return nil }), true); (err != nil) != tt.wantErr { - t.Errorf("CRDB.query() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("eventstore.query() error = %v, wantErr %v", err, tt.wantErr) } }) } } -/* Cockroach test DB doesn't seem to lock -func Test_query_events_with_crdb_locking(t *testing.T) { - type args struct { - searchQuery *eventstore.SearchQueryBuilder - } - type fields struct { - existingEvents []eventstore.Command - client *sql.DB - } - tests := []struct { - name string - fields fields - args args - lockOption eventstore.LockOption - wantErr bool - }{ - { - name: "skip locked", - fields: fields{ - client: testCRDBClient, - existingEvents: []eventstore.Command{ - generateEvent(t, "306", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), - generateEvent(t, "307", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), - generateEvent(t, "308", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), - }, - }, - args: args{ - searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - ResourceOwner("caos"), - }, - lockOption: eventstore.LockOptionNoWait, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db := &CRDB{ - DB: &database.DB{ - DB: tt.fields.client, - Database: new(testDB), - }, - } - // setup initial data for query - if _, err := db.Push(context.Background(), tt.fields.existingEvents...); err != nil { - t.Errorf("error in setup = %v", err) - return - } - // first TX should lock and return all events - tx1, err := db.DB.Begin() - require.NoError(t, err) - defer func() { - require.NoError(t, tx1.Rollback()) - }() - searchQuery1 := tt.args.searchQuery.LockRowsDuringTx(tx1, tt.lockOption) - gotEvents1 := []eventstore.Event{} - err = query(context.Background(), db, searchQuery1, eventstore.Reducer(func(event eventstore.Event) error { - gotEvents1 = append(gotEvents1, event) - return nil - }), true) - require.NoError(t, err) - assert.Len(t, gotEvents1, len(tt.fields.existingEvents)) - - // second TX should not return the events, and might return an error - tx2, err := db.DB.Begin() - require.NoError(t, err) - defer func() { - require.NoError(t, tx2.Rollback()) - }() - searchQuery2 := tt.args.searchQuery.LockRowsDuringTx(tx1, tt.lockOption) - gotEvents2 := []eventstore.Event{} - err = query(context.Background(), db, searchQuery2, eventstore.Reducer(func(event eventstore.Event) error { - gotEvents2 = append(gotEvents2, event) - return nil - }), true) - if tt.wantErr { - require.Error(t, err) - } - require.NoError(t, err) - assert.Len(t, gotEvents2, 0) - }) - } -} -*/ - func Test_query_events_mocked(t *testing.T) { type args struct { query *eventstore.SearchQueryBuilder - dest interface{} + dest any useV1 bool } type res struct { @@ -772,8 +691,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY($2) AND state <> 'idle') ORDER BY event_sequence DESC`), []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}}, ), }, @@ -795,8 +714,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence LIMIT \$3`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY($2) AND state <> 'idle') ORDER BY event_sequence LIMIT $3`), []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}, uint64(5)}, ), }, @@ -818,32 +737,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC LIMIT \$3`, - []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}, uint64(5)}, - ), - }, - res: res{ - wantErr: false, - }, - }, - { - name: "with limit and order by desc as of system time", - args: args{ - dest: &[]*repository.Event{}, - query: eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - OrderDesc(). - AwaitOpenTransactions(). - Limit(5). - AllowTimeTravel(). - AddQuery(). - AggregateTypes("user"). - Builder(), - useV1: true, - }, - fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events AS OF SYSTEM TIME '-1 ms' WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC LIMIT \$3`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY($2) AND state <> 'idle') ORDER BY event_sequence DESC LIMIT $3`), []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}, uint64(5)}, ), }, @@ -864,8 +759,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 ORDER BY event_sequence DESC LIMIT \$2 FOR UPDATE`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 ORDER BY event_sequence DESC LIMIT $2 FOR UPDATE`), []driver.Value{eventstore.AggregateType("user"), uint64(5)}, ), }, @@ -886,8 +781,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 ORDER BY event_sequence DESC LIMIT \$2 FOR UPDATE NOWAIT`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 ORDER BY event_sequence DESC LIMIT $2 FOR UPDATE NOWAIT`), []driver.Value{eventstore.AggregateType("user"), uint64(5)}, ), }, @@ -908,8 +803,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 ORDER BY event_sequence DESC LIMIT \$2 FOR UPDATE SKIP LOCKED`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 ORDER BY event_sequence DESC LIMIT $2 FOR UPDATE SKIP LOCKED`), []driver.Value{eventstore.AggregateType("user"), uint64(5)}, ), }, @@ -931,8 +826,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQueryErr(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC`, + mock: newMockClient(t).expectQueryErr( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY($2) AND state <> 'idle') ORDER BY event_sequence DESC`), []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}}, sql.ErrConnDone), }, @@ -954,8 +849,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQueryScanErr(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC`, + mock: newMockClient(t).expectQueryScanErr( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY($2) AND state <> 'idle') ORDER BY event_sequence DESC`), []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}}, &repository.Event{Seq: 100}), }, @@ -989,8 +884,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE \(aggregate_type = \$1 OR \(aggregate_type = \$2 AND aggregate_id = \$3\)\) AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$4\)\) ORDER BY event_sequence DESC LIMIT \$5`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE (aggregate_type = $1 OR (aggregate_type = $2 AND aggregate_id = $3)) AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY($4) AND state <> 'idle') ORDER BY event_sequence DESC LIMIT $5`), []driver.Value{eventstore.AggregateType("user"), eventstore.AggregateType("org"), "asdf42", database.TextArray[string]{}, uint64(5)}, ), }, @@ -1018,10 +913,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - regexp.QuoteMeta( - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY event_sequence DESC LIMIT $9`, - ), + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY event_sequence DESC LIMIT $9`), []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), 123.456, eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", 123.456, uint64(5)}, ), }, @@ -1049,10 +942,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: false, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - regexp.QuoteMeta( - `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`, - ), + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`), []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), 123.456, eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", 123.456, uint64(5)}, ), }, @@ -1080,10 +971,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: false, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - regexp.QuoteMeta( - `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND created_at > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND created_at > $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`, - ), + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND created_at > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND created_at > $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`), []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), time.Unix(123, 456), eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", time.Unix(123, 456), uint64(5)}, ), }, @@ -1092,14 +981,14 @@ func Test_query_events_mocked(t *testing.T) { }, }, } - crdb := NewCRDB(&database.DB{Database: new(testDB)}) + client := NewPostgres(&database.DB{Database: new(testDB)}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.fields.mock != nil { - crdb.DB.DB = tt.fields.mock.client + client.DB.DB = tt.fields.mock.client } - err := query(context.Background(), crdb, tt.args.query, tt.args.dest, tt.args.useV1) + err := query(context.Background(), client, tt.args.query, tt.args.dest, tt.args.useV1) if (err != nil) != tt.res.wantErr { t.Errorf("query() error = %v, wantErr %v", err, tt.res.wantErr) } @@ -1120,7 +1009,7 @@ type dbMock struct { client *sql.DB } -func (m *dbMock) expectQuery(t *testing.T, expectedQuery string, args []driver.Value, events ...*repository.Event) *dbMock { +func (m *dbMock) expectQuery(expectedQuery string, args []driver.Value, events ...*repository.Event) *dbMock { query := m.mock.ExpectQuery(expectedQuery).WithArgs(args...) rows := m.mock.NewRows([]string{"sequence"}) for _, event := range events { @@ -1130,7 +1019,7 @@ func (m *dbMock) expectQuery(t *testing.T, expectedQuery string, args []driver.V return m } -func (m *dbMock) expectQueryScanErr(t *testing.T, expectedQuery string, args []driver.Value, events ...*repository.Event) *dbMock { +func (m *dbMock) expectQueryScanErr(expectedQuery string, args []driver.Value, events ...*repository.Event) *dbMock { query := m.mock.ExpectQuery(expectedQuery).WithArgs(args...) rows := m.mock.NewRows([]string{"sequence"}) for _, event := range events { @@ -1140,7 +1029,7 @@ func (m *dbMock) expectQueryScanErr(t *testing.T, expectedQuery string, args []d return m } -func (m *dbMock) expectQueryErr(t *testing.T, expectedQuery string, args []driver.Value, err error) *dbMock { +func (m *dbMock) expectQueryErr(expectedQuery string, args []driver.Value, err error) *dbMock { m.mock.ExpectQuery(expectedQuery).WithArgs(args...).WillReturnError(err) return m } diff --git a/internal/eventstore/search_query.go b/internal/eventstore/search_query.go index df38d15def..1596936a36 100644 --- a/internal/eventstore/search_query.go +++ b/internal/eventstore/search_query.go @@ -25,7 +25,6 @@ type SearchQueryBuilder struct { tx *sql.Tx lockRows bool lockOption LockOption - allowTimeTravel bool positionAfter float64 awaitOpenTransactions bool creationDateAfter time.Time @@ -77,10 +76,6 @@ func (b *SearchQueryBuilder) GetTx() *sql.Tx { return b.tx } -func (b *SearchQueryBuilder) GetAllowTimeTravel() bool { - return b.allowTimeTravel -} - func (b SearchQueryBuilder) GetPositionAfter() float64 { return b.positionAfter } @@ -289,13 +284,6 @@ func (builder *SearchQueryBuilder) EditorUser(id string) *SearchQueryBuilder { return builder } -// AllowTimeTravel activates the time travel feature of the database if supported -// The queries will be made based on the call time -func (builder *SearchQueryBuilder) AllowTimeTravel() *SearchQueryBuilder { - builder.allowTimeTravel = true - return builder -} - // PositionAfter filters for events which happened after the specified time func (builder *SearchQueryBuilder) PositionAfter(position float64) *SearchQueryBuilder { builder.positionAfter = position diff --git a/internal/eventstore/search_query_test.go b/internal/eventstore/search_query_test.go index 8c654911ea..b8f570dc0d 100644 --- a/internal/eventstore/search_query_test.go +++ b/internal/eventstore/search_query_test.go @@ -45,16 +45,6 @@ func testSetLimit(limit uint64) func(builder *SearchQueryBuilder) *SearchQueryBu } } -func testOr(queryFuncs ...func(*SearchQuery) *SearchQuery) func(*SearchQuery) *SearchQuery { - return func(query *SearchQuery) *SearchQuery { - subQuery := query.Or() - for _, queryFunc := range queryFuncs { - queryFunc(subQuery) - } - return subQuery - } -} - func testSetAggregateTypes(types ...AggregateType) func(*SearchQuery) *SearchQuery { return func(query *SearchQuery) *SearchQuery { query = query.AggregateTypes(types...) diff --git a/internal/eventstore/subscription.go b/internal/eventstore/subscription.go index c76c81df19..076d16ad52 100644 --- a/internal/eventstore/subscription.go +++ b/internal/eventstore/subscription.go @@ -1,6 +1,7 @@ package eventstore import ( + "slices" "sync" "github.com/zitadel/logging" @@ -8,7 +9,7 @@ import ( var ( subscriptions = map[AggregateType][]*Subscription{} - subsMutext sync.Mutex + subsMutex sync.RWMutex ) type Subscription struct { @@ -27,8 +28,8 @@ func SubscribeAggregates(eventQueue chan Event, aggregates ...AggregateType) *Su types: types, } - subsMutext.Lock() - defer subsMutext.Unlock() + subsMutex.Lock() + defer subsMutex.Unlock() for _, aggregate := range aggregates { subscriptions[aggregate] = append(subscriptions[aggregate], sub) @@ -45,8 +46,8 @@ func SubscribeEventTypes(eventQueue chan Event, types map[AggregateType][]EventT types: types, } - subsMutext.Lock() - defer subsMutext.Unlock() + subsMutex.Lock() + defer subsMutex.Unlock() for aggregate := range types { subscriptions[aggregate] = append(subscriptions[aggregate], sub) @@ -56,8 +57,8 @@ func SubscribeEventTypes(eventQueue chan Event, types map[AggregateType][]EventT } func (es *Eventstore) notify(events []Event) { - subsMutext.Lock() - defer subsMutext.Unlock() + subsMutex.RLock() + defer subsMutex.RUnlock() for _, event := range events { subs, ok := subscriptions[event.Aggregate().Type] if !ok { @@ -71,14 +72,11 @@ func (es *Eventstore) notify(events []Event) { continue } //subscription for certain events - for _, eventType := range eventTypes { - if event.Type() == eventType { - select { - case sub.Events <- event: - default: - logging.Debug("unable to push event") - } - break + if slices.Contains(eventTypes, event.Type()) { + select { + case sub.Events <- event: + default: + logging.Debug("unable to push event") } } } @@ -86,8 +84,8 @@ func (es *Eventstore) notify(events []Event) { } func (s *Subscription) Unsubscribe() { - subsMutext.Lock() - defer subsMutext.Unlock() + subsMutex.Lock() + defer subsMutex.Unlock() for aggregate := range s.types { subs, ok := subscriptions[aggregate] if !ok { diff --git a/internal/eventstore/v3/eventstore.go b/internal/eventstore/v3/eventstore.go index 1bb515527c..424805c882 100644 --- a/internal/eventstore/v3/eventstore.go +++ b/internal/eventstore/v3/eventstore.go @@ -24,9 +24,9 @@ func init() { var ( // pushPlaceholderFmt defines how data are inserted into the events table - pushPlaceholderFmt string + pushPlaceholderFmt = "($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $%d)" // uniqueConstraintPlaceholderFmt defines the format of the unique constraint error returned from the database - uniqueConstraintPlaceholderFmt string + uniqueConstraintPlaceholderFmt = "(%s, %s, %s)" _ eventstore.Pusher = (*Eventstore)(nil) ) @@ -158,15 +158,6 @@ func (es *Eventstore) Client() *database.DB { } func NewEventstore(client *database.DB) *Eventstore { - switch client.Type() { - case "cockroach": - pushPlaceholderFmt = "($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp(), $%d)" - uniqueConstraintPlaceholderFmt = "('%s', '%s', '%s')" - case "postgres": - pushPlaceholderFmt = "($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $%d)" - uniqueConstraintPlaceholderFmt = "(%s, %s, %s)" - } - return &Eventstore{client: client} } @@ -200,14 +191,8 @@ func (es *Eventstore) pushTx(ctx context.Context, client database.ContextQueryEx beginner = es.client } - isolationLevel := sql.LevelReadCommitted - // cockroach requires serializable to execute the push function - // because we use [cluster_logical_timestamp()](https://www.cockroachlabs.com/docs/stable/functions-and-operators#system-info-functions) - if es.client.Type() == "cockroach" { - isolationLevel = sql.LevelSerializable - } tx, err = beginner.BeginTx(ctx, &sql.TxOptions{ - Isolation: isolationLevel, + Isolation: sql.LevelReadCommitted, ReadOnly: false, }) if err != nil { diff --git a/internal/eventstore/v3/field.go b/internal/eventstore/v3/field.go index b399e7f5e8..e8f761d410 100644 --- a/internal/eventstore/v3/field.go +++ b/internal/eventstore/v3/field.go @@ -47,7 +47,7 @@ func (es *Eventstore) FillFields(ctx context.Context, events ...eventstore.FillF // Search implements the [eventstore.Search] method func (es *Eventstore) Search(ctx context.Context, conditions ...map[eventstore.FieldType]any) (result []*eventstore.SearchResult, err error) { ctx, span := tracing.NewSpan(ctx) - defer span.EndWithError(err) + defer func() { span.EndWithError(err) }() var builder strings.Builder args := buildSearchStatement(ctx, &builder, conditions...) @@ -156,10 +156,11 @@ func (es *Eventstore) handleFieldCommands(ctx context.Context, tx database.Tx, c func handleFieldFillEvents(ctx context.Context, tx database.Tx, events []eventstore.FillFieldsEvent) error { for _, event := range events { - if len(event.Fields()) > 0 { - if err := handleFieldOperations(ctx, tx, event.Fields()); err != nil { - return err - } + if len(event.Fields()) == 0 { + continue + } + if err := handleFieldOperations(ctx, tx, event.Fields()); err != nil { + return err } } return nil diff --git a/internal/eventstore/v3/push_test.go b/internal/eventstore/v3/push_test.go index a6c4f515fd..da583891e9 100644 --- a/internal/eventstore/v3/push_test.go +++ b/internal/eventstore/v3/push_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/database/cockroach" + "github.com/zitadel/zitadel/internal/database/postgres" "github.com/zitadel/zitadel/internal/eventstore" ) @@ -65,7 +65,7 @@ func Test_mapCommands(t *testing.T) { ), }, placeHolders: []string{ - "($1, $2, $3, $4, $5, $6, $7, $8, $9, hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp(), $10)", + "($1, $2, $3, $4, $5, $6, $7, $8, $9, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $10)", }, args: []any{ "instance", @@ -114,8 +114,8 @@ func Test_mapCommands(t *testing.T) { ), }, placeHolders: []string{ - "($1, $2, $3, $4, $5, $6, $7, $8, $9, hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp(), $10)", - "($11, $12, $13, $14, $15, $16, $17, $18, $19, hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp(), $20)", + "($1, $2, $3, $4, $5, $6, $7, $8, $9, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $10)", + "($11, $12, $13, $14, $15, $16, $17, $18, $19, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $20)", }, args: []any{ // first event @@ -180,8 +180,8 @@ func Test_mapCommands(t *testing.T) { ), }, placeHolders: []string{ - "($1, $2, $3, $4, $5, $6, $7, $8, $9, hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp(), $10)", - "($11, $12, $13, $14, $15, $16, $17, $18, $19, hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp(), $20)", + "($1, $2, $3, $4, $5, $6, $7, $8, $9, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $10)", + "($11, $12, $13, $14, $15, $16, $17, $18, $19, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $20)", }, args: []any{ // first event @@ -236,7 +236,7 @@ func Test_mapCommands(t *testing.T) { } } // is used to set the the [pushPlaceholderFmt] - NewEventstore(&database.DB{Database: new(cockroach.Config)}) + NewEventstore(&database.DB{Database: new(postgres.Config)}) t.Run(tt.name, func(t *testing.T) { defer func() { cause := recover() diff --git a/internal/eventstore/v3/push_without_func.go b/internal/eventstore/v3/push_without_func.go index 914b880204..b94a9e8f54 100644 --- a/internal/eventstore/v3/push_without_func.go +++ b/internal/eventstore/v3/push_without_func.go @@ -7,7 +7,6 @@ import ( "fmt" "strings" - "github.com/cockroachdb/cockroach-go/v2/crdb" "github.com/jackc/pgx/v5/pgconn" "github.com/zitadel/logging" @@ -16,25 +15,6 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -type transaction struct { - database.Tx -} - -var _ crdb.Tx = (*transaction)(nil) - -func (t *transaction) Exec(ctx context.Context, query string, args ...interface{}) error { - _, err := t.Tx.ExecContext(ctx, query, args...) - return err -} - -func (t *transaction) Commit(ctx context.Context) error { - return t.Tx.Commit() -} - -func (t *transaction) Rollback(ctx context.Context) error { - return t.Tx.Rollback() -} - // checks whether the error is caused because setup step 39 was not executed func isSetupNotExecutedError(err error) bool { if err == nil { @@ -64,7 +44,6 @@ func (es *Eventstore) pushWithoutFunc(ctx context.Context, client database.Conte err = closeTx(err) }() - // tx is not closed because [crdb.ExecuteInTx] takes care of that var ( sequences []*latestSequence ) diff --git a/internal/execution/ctx.go b/internal/execution/ctx.go new file mode 100644 index 0000000000..9e6bac3e30 --- /dev/null +++ b/internal/execution/ctx.go @@ -0,0 +1,19 @@ +package execution + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ExecutionUserID = "EXECUTION" + +func HandlerContext(event *eventstore.Aggregate) context.Context { + ctx := authz.WithInstanceID(context.Background(), event.InstanceID) + return authz.SetCtxData(ctx, authz.CtxData{UserID: ExecutionUserID, OrgID: event.ResourceOwner}) +} + +func ContextWithExecuter(ctx context.Context, aggregate *eventstore.Aggregate) context.Context { + return authz.SetCtxData(ctx, authz.CtxData{UserID: ExecutionUserID, OrgID: aggregate.ResourceOwner}) +} diff --git a/internal/execution/execution.go b/internal/execution/execution.go index 99d7f6182f..b885858d94 100644 --- a/internal/execution/execution.go +++ b/internal/execution/execution.go @@ -6,12 +6,15 @@ import ( "encoding/json" "io" "net/http" + "strings" "time" "github.com/zitadel/logging" zhttp "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/pkg/actions" @@ -39,7 +42,7 @@ func CallTargets( info ContextInfo, ) (_ interface{}, err error) { ctx, span := tracing.NewSpan(ctx) - defer span.EndWithError(err) + defer func() { span.EndWithError(err) }() for _, target := range targets { // call the type of target @@ -69,7 +72,7 @@ func CallTarget( info ContextInfoRequest, ) (res []byte, err error) { ctx, span := tracing.NewSpan(ctx) - defer span.EndWithError(err) + defer func() { span.EndWithError(err) }() switch target.GetTargetType() { // get request, ignore response and return request and error for handling in list of targets @@ -79,11 +82,11 @@ func CallTarget( case domain.TargetTypeCall: return Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), target.GetSigningKey()) case domain.TargetTypeAsync: - go func(target Target, info ContextInfoRequest) { - if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), target.GetSigningKey()); err != nil { + go func(ctx context.Context, target Target, info []byte) { + if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info, target.GetSigningKey()); err != nil { logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err) } - }(target, info) + }(context.WithoutCancel(ctx), target, info.GetHTTPRequestBody()) return nil, nil default: return nil, zerrors.ThrowInternal(nil, "EXEC-auqnansr2m", "Errors.Execution.Unknown") @@ -153,3 +156,59 @@ type ErrorBody struct { ForwardedStatusCode int `json:"forwardedStatusCode,omitempty"` ForwardedErrorMessage string `json:"forwardedErrorMessage,omitempty"` } + +type ExecutionTargetsQueries interface { + TargetsByExecutionID(ctx context.Context, ids []string) (execution []*query.ExecutionTarget, err error) + TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string) (execution []*query.ExecutionTarget, err error) +} + +func QueryExecutionTargetsForRequestAndResponse( + ctx context.Context, + queries ExecutionTargetsQueries, + fullMethod string, +) ([]Target, []Target) { + ctx, span := tracing.NewSpan(ctx) + defer span.End() + + targets, err := queries.TargetsByExecutionIDs(ctx, + idsForFullMethod(fullMethod, domain.ExecutionTypeRequest), + idsForFullMethod(fullMethod, domain.ExecutionTypeResponse), + ) + requestTargets := make([]Target, 0, len(targets)) + responseTargets := make([]Target, 0, len(targets)) + if err != nil { + logging.WithFields("fullMethod", fullMethod).WithError(err).Info("unable to query targets") + return requestTargets, responseTargets + } + + for _, target := range targets { + if strings.HasPrefix(target.GetExecutionID(), execution.IDAll(domain.ExecutionTypeRequest)) { + requestTargets = append(requestTargets, target) + } else if strings.HasPrefix(target.GetExecutionID(), execution.IDAll(domain.ExecutionTypeResponse)) { + responseTargets = append(responseTargets, target) + } + } + + return requestTargets, responseTargets +} + +func idsForFullMethod(fullMethod string, executionType domain.ExecutionType) []string { + return []string{execution.ID(executionType, fullMethod), execution.ID(executionType, serviceFromFullMethod(fullMethod)), execution.IDAll(executionType)} +} + +func serviceFromFullMethod(s string) string { + parts := strings.Split(s, "/") + return parts[1] +} + +func QueryExecutionTargetsForFunction(ctx context.Context, query ExecutionTargetsQueries, function string) ([]Target, error) { + queriedActionsV2, err := query.TargetsByExecutionID(ctx, []string{function}) + if err != nil { + return nil, err + } + executionTargets := make([]Target, len(queriedActionsV2)) + for i, action := range queriedActionsV2 { + executionTargets[i] = action + } + return executionTargets, nil +} diff --git a/internal/execution/execution_test.go b/internal/execution/execution_test.go index 5a45d96625..036b160ab7 100644 --- a/internal/execution/execution_test.go +++ b/internal/execution/execution_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" "github.com/zitadel/zitadel/internal/domain" @@ -61,7 +62,7 @@ func Test_Call(t *testing.T) { args{ ctx: context.Background(), timeout: time.Second, - sleep: time.Second, + sleep: 2 * time.Second, method: http.MethodPost, body: []byte("{\"request\": \"values\"}"), respBody: []byte("{\"response\": \"values\"}"), @@ -149,8 +150,8 @@ func Test_CallTarget(t *testing.T) { info: requestContextInfo1, server: &callTestServer{ method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), timeout: time.Second, statusCode: http.StatusInternalServerError, }, @@ -170,8 +171,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusInternalServerError, }, target: &mockTarget{ @@ -191,8 +192,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusOK, }, target: &mockTarget{ @@ -212,8 +213,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusOK, signingKey: "signingkey", }, @@ -235,8 +236,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusInternalServerError, }, target: &mockTarget{ @@ -256,8 +257,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusOK, }, target: &mockTarget{ @@ -266,7 +267,7 @@ func Test_CallTarget(t *testing.T) { }, }, res{ - body: []byte("{\"request\":\"content2\"}"), + body: []byte("{\"content\":\"request2\"}"), }, }, { @@ -277,8 +278,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusOK, signingKey: "signingkey", }, @@ -289,7 +290,7 @@ func Test_CallTarget(t *testing.T) { }, }, res{ - body: []byte("{\"request\":\"content2\"}"), + body: []byte("{\"content\":\"request2\"}"), }, }, } @@ -576,13 +577,13 @@ func testCallTargets(ctx context.Context, } var requestContextInfo1 = &middleware.ContextInfoRequest{ - Request: &request{ - Request: "content1", - }, + Request: middleware.Message{Message: &structpb.Struct{ + Fields: map[string]*structpb.Value{"content": structpb.NewStringValue("request1")}, + }}, } -var requestContextInfoBody1 = []byte("{\"request\":{\"request\":\"content1\"}}") -var requestContextInfoBody2 = []byte("{\"request\":{\"request\":\"content2\"}}") +var requestContextInfoBody1 = []byte("{\"request\":{\"content\":\"request1\"}}") +var requestContextInfoBody2 = []byte("{\"request\":{\"content\":\"request2\"}}") type request struct { Request string `json:"request"` diff --git a/internal/execution/gen_mock.go b/internal/execution/gen_mock.go new file mode 100644 index 0000000000..93eebfbb02 --- /dev/null +++ b/internal/execution/gen_mock.go @@ -0,0 +1,4 @@ +package execution + +//go:generate mockgen -package mock -destination ./mock/queries.mock.go github.com/zitadel/zitadel/internal/execution Queries +//go:generate mockgen -package mock -destination ./mock/queue.mock.go github.com/zitadel/zitadel/internal/execution Queue diff --git a/internal/execution/handlers.go b/internal/execution/handlers.go new file mode 100644 index 0000000000..7ffb4cc6ff --- /dev/null +++ b/internal/execution/handlers.go @@ -0,0 +1,156 @@ +package execution + +import ( + "context" + "encoding/json" + "slices" + "strings" + + "github.com/riverqueue/river" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/queue" + exec_repo "github.com/zitadel/zitadel/internal/repository/execution" +) + +const ( + HandlerTable = "projections.execution_handler" +) + +type Queue interface { + Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error +} + +type Queries interface { + TargetsByExecutionID(ctx context.Context, ids []string) (execution []*query.ExecutionTarget, err error) + InstanceByID(ctx context.Context, id string) (instance authz.Instance, err error) +} + +type eventHandler struct { + eventTypes []string + aggregateTypeFromEventType func(typ eventstore.EventType) eventstore.AggregateType + query Queries + queue Queue +} + +func NewEventHandler( + ctx context.Context, + config handler.Config, + eventTypes []string, + aggregateTypeFromEventType func(typ eventstore.EventType) eventstore.AggregateType, + query Queries, + queue Queue, +) *handler.Handler { + return handler.NewHandler(ctx, &config, &eventHandler{ + eventTypes: eventTypes, + aggregateTypeFromEventType: aggregateTypeFromEventType, + query: query, + queue: queue, + }) +} + +func (u *eventHandler) Name() string { + return HandlerTable +} + +func (u *eventHandler) Reducers() []handler.AggregateReducer { + aggList := make(map[eventstore.AggregateType][]eventstore.EventType) + for _, eventType := range u.eventTypes { + aggType := u.aggregateTypeFromEventType(eventstore.EventType(eventType)) + aggEventTypes := aggList[aggType] + if !slices.Contains(aggEventTypes, eventstore.EventType(eventType)) { + aggList[aggType] = append(aggList[aggType], eventstore.EventType(eventType)) + } + } + + aggReducers := make([]handler.AggregateReducer, 0, len(aggList)) + for aggType, aggEventTypes := range aggList { + eventReducers := make([]handler.EventReducer, len(aggEventTypes)) + for j, eventType := range aggEventTypes { + eventReducers[j] = handler.EventReducer{ + Event: eventType, + Reduce: u.reduce, + } + } + aggReducers = append(aggReducers, handler.AggregateReducer{ + Aggregate: aggType, + EventReducers: eventReducers, + }) + } + return aggReducers +} + +func groupsFromEventType(s string) []string { + parts := strings.Split(s, ".") + groups := make([]string, len(parts)) + for i := range parts { + groups[i] = strings.Join(parts[:i+1], ".") + if i < len(parts)-1 { + groups[i] += ".*" + } + } + slices.Reverse(groups) + return groups +} + +func idsForEventType(eventType string) []string { + ids := make([]string, 0) + for _, group := range groupsFromEventType(eventType) { + ids = append(ids, + exec_repo.ID(domain.ExecutionTypeEvent, group), + ) + } + return append(ids, + exec_repo.IDAll(domain.ExecutionTypeEvent), + ) +} + +func (u *eventHandler) reduce(e eventstore.Event) (*handler.Statement, error) { + ctx := HandlerContext(e.Aggregate()) + + targets, err := u.query.TargetsByExecutionID(ctx, idsForEventType(string(e.Type()))) + if err != nil { + return nil, err + } + + // no execution from worker necessary + if len(targets) == 0 { + return handler.NewNoOpStatement(e), nil + } + + return handler.NewStatement(e, func(ex handler.Executer, projectionName string) error { + ctx := HandlerContext(e.Aggregate()) + req, err := NewRequest(e, targets) + if err != nil { + return err + } + return u.queue.Insert(ctx, + req, + queue.WithQueueName(exec_repo.QueueName), + ) + }), nil +} + +func NewRequest(e eventstore.Event, targets []*query.ExecutionTarget) (*exec_repo.Request, error) { + targetsData, err := json.Marshal(targets) + if err != nil { + return nil, err + } + eventData, err := json.Marshal(e) + if err != nil { + return nil, err + } + return &exec_repo.Request{ + Aggregate: e.Aggregate(), + Sequence: e.Sequence(), + EventType: e.Type(), + CreatedAt: e.CreatedAt(), + UserID: e.Creator(), + EventData: eventData, + TargetsData: targetsData, + }, nil +} diff --git a/internal/execution/handlers_test.go b/internal/execution/handlers_test.go new file mode 100644 index 0000000000..de220abcc0 --- /dev/null +++ b/internal/execution/handlers_test.go @@ -0,0 +1,487 @@ +package execution + +import ( + "database/sql" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/execution/mock" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/repository/action" + execution_rp "github.com/zitadel/zitadel/internal/repository/execution" + "github.com/zitadel/zitadel/internal/repository/session" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func Test_EventExecution(t *testing.T) { + type args struct { + event eventstore.Event + targets []*query.ExecutionTarget + } + type res struct { + targets []Target + contextInfo *execution_rp.ContextInfoEvent + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + "session added, ok", + args{ + event: &eventstore.BaseEvent{ + Agg: &eventstore.Aggregate{ + ID: "aggID", + Type: session.AggregateType, + ResourceOwner: "resourceOwner", + InstanceID: "instanceID", + Version: session.AggregateVersion, + }, + EventType: session.AddedType, + Seq: 1, + Creation: time.Date(2024, 1, 1, 1, 1, 1, 1, time.UTC), + User: userID, + Data: []byte(`{"ID":"","Seq":1,"Pos":0,"Creation":"2024-01-01T01:01:01.000000001Z"}`), + }, + targets: []*query.ExecutionTarget{{ + InstanceID: instanceID, + ExecutionID: "executionID", + TargetID: "targetID", + TargetType: domain.TargetTypeWebhook, + Endpoint: "endpoint", + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "key", + }}, + }, + res{ + targets: []Target{ + &query.ExecutionTarget{ + InstanceID: instanceID, + ExecutionID: "executionID", + TargetID: "targetID", + TargetType: domain.TargetTypeWebhook, + Endpoint: "endpoint", + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "key", + }, + }, + contextInfo: &execution_rp.ContextInfoEvent{ + AggregateID: "aggID", + AggregateType: "session", + ResourceOwner: "resourceOwner", + InstanceID: "instanceID", + Version: "v1", + Sequence: 1, + EventType: "session.added", + CreatedAt: time.Date(2024, 1, 1, 1, 1, 1, 1, time.UTC).Format(time.RFC3339Nano), + UserID: userID, + EventPayload: []byte(`{"ID":"","Seq":1,"Pos":0,"Creation":"2024-01-01T01:01:01.000000001Z"}`), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + request, err := NewRequest(tt.args.event, tt.args.targets) + if tt.res.wantErr { + assert.Error(t, err) + assert.Nil(t, request) + return + } + assert.NoError(t, err) + targets, err := TargetsFromRequest(request) + assert.NoError(t, err) + assert.Equal(t, tt.res.targets, targets) + assert.Equal(t, tt.res.contextInfo, execution_rp.ContextInfoFromRequest(request)) + }) + } +} + +func Test_groupsFromEventType(t *testing.T) { + type args struct { + eventType eventstore.EventType + } + type res struct { + groups []string + } + tests := []struct { + name string + args args + res res + }{ + { + "user human mfa init skipped, ok", + args{ + eventType: user.HumanMFAInitSkippedType, + }, + res{ + groups: []string{ + "user.human.mfa.init.skipped", + "user.human.mfa.init.*", + "user.human.mfa.*", + "user.human.*", + "user.*", + }, + }, + }, + { + "session added, ok", + args{ + eventType: session.AddedType, + }, + res{ + groups: []string{ + "session.added", + "session.*", + }, + }, + }, + { + "user added, ok", + args{ + eventType: user.HumanAddedType, + }, + res{ + groups: []string{ + "user.human.added", + "user.human.*", + "user.*", + }, + }, + }, + { + "execution set, ok", + args{ + eventType: execution_rp.SetEventV2Type, + }, + res{ + groups: []string{ + "execution.v2.set", + "execution.v2.*", + "execution.*", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.res.groups, groupsFromEventType(string(tt.args.eventType))) + }) + } +} + +func Test_idsForEventType(t *testing.T) { + type args struct { + eventType eventstore.EventType + } + type res struct { + groups []string + } + tests := []struct { + name string + args args + res res + }{ + { + "session added, ok", + args{ + eventType: session.AddedType, + }, + res{ + groups: []string{ + "event/session.added", + "event/session.*", + "event", + }, + }, + }, + { + "user added, ok", + args{ + eventType: user.HumanAddedType, + }, + res{ + groups: []string{ + "event/user.human.added", + "event/user.human.*", + "event/user.*", + "event", + }, + }, + }, + { + "execution set, ok", + args{ + eventType: execution_rp.SetEventV2Type, + }, + res{ + groups: []string{ + "event/execution.v2.set", + "event/execution.v2.*", + "event/execution.*", + "event", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.res.groups, idsForEventType(string(tt.args.eventType))) + }) + } +} + +func TestActionProjection_reduces(t *testing.T) { + tests := []struct { + name string + test func(*gomock.Controller, *mock.MockQueries, *mock.MockQueue) (fields, args, want) + }{ + { + name: "reduce, action, error", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, q *mock.MockQueue) (f fields, a args, w want) { + queries.EXPECT().TargetsByExecutionID(gomock.Any(), gomock.Any()).Return(nil, zerrors.ThrowInternal(nil, "QUERY-37ardr0pki", "Errors.Query.CloseRows")) + return fields{ + queries: queries, + queue: q, + }, args{ + event: &action.AddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: eventID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: action.AddedEventType, + Data: []byte(eventData), + EditorUser: userID, + Seq: 1, + AggregateType: action.AggregateType, + Version: action.AggregateVersion, + }), + Name: "name", + Script: "name(){}", + Timeout: 3 * time.Second, + AllowedToFail: true, + }, + mapper: action.AddedEventMapper, + }, want{ + err: func(tt assert.TestingT, err error, i ...interface{}) bool { + return errors.Is(err, zerrors.ThrowInternal(nil, "QUERY-37ardr0pki", "Errors.Query.CloseRows")) + }, + } + }, + }, + + { + name: "reduce, action, none", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, q *mock.MockQueue) (f fields, a args, w want) { + queries.EXPECT().TargetsByExecutionID(gomock.Any(), gomock.Any()).Return([]*query.ExecutionTarget{}, nil) + return fields{ + queries: queries, + queue: q, + }, args{ + event: &action.AddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: eventID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: action.AddedEventType, + Data: []byte(eventData), + EditorUser: userID, + Seq: 1, + AggregateType: action.AggregateType, + Version: action.AggregateVersion, + }), + Name: "name", + Script: "name(){}", + Timeout: 3 * time.Second, + AllowedToFail: true, + }, + mapper: action.AddedEventMapper, + }, want{ + noOperation: true, + } + }, + }, + { + name: "reduce, action, single", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, q *mock.MockQueue) (f fields, a args, w want) { + targets := mockTargets(1) + queries.EXPECT().TargetsByExecutionID(gomock.Any(), gomock.Any()).Return(targets, nil) + createdAt := time.Now().UTC() + q.EXPECT().Insert( + gomock.Any(), + &execution_rp.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + Type: action.AggregateType, + Version: action.AggregateVersion, + ID: eventID, + ResourceOwner: orgID, + }, + Sequence: 1, + CreatedAt: createdAt, + EventType: action.AddedEventType, + UserID: userID, + EventData: []byte(eventData), + TargetsData: mockTargetsToBytes(targets), + }, + gomock.Any(), + ).Return(nil) + return fields{ + queries: queries, + queue: q, + }, args{ + event: &action.AddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: eventID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: createdAt, + Typ: action.AddedEventType, + Data: []byte(eventData), + EditorUser: userID, + Seq: 1, + AggregateType: action.AggregateType, + Version: action.AggregateVersion, + }), + Name: "name", + Script: "name(){}", + Timeout: 3 * time.Second, + AllowedToFail: true, + }, + mapper: action.AddedEventMapper, + }, w + }, + }, + { + name: "reduce, action, multiple", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, q *mock.MockQueue) (f fields, a args, w want) { + targets := mockTargets(3) + queries.EXPECT().TargetsByExecutionID(gomock.Any(), gomock.Any()).Return(targets, nil) + createdAt := time.Now().UTC() + q.EXPECT().Insert( + gomock.Any(), + &execution_rp.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + Type: action.AggregateType, + Version: action.AggregateVersion, + ID: eventID, + ResourceOwner: orgID, + }, + Sequence: 1, + CreatedAt: createdAt, + EventType: action.AddedEventType, + UserID: userID, + EventData: []byte(eventData), + TargetsData: mockTargetsToBytes(targets), + }, + gomock.Any(), + ).Return(nil) + return fields{ + queries: queries, + queue: q, + }, args{ + event: &action.AddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: eventID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: createdAt, + Typ: action.AddedEventType, + Data: []byte(eventData), + EditorUser: userID, + Seq: 1, + AggregateType: action.AggregateType, + Version: action.AggregateVersion, + }), + Name: "name", + Script: "name(){}", + Timeout: 3 * time.Second, + AllowedToFail: true, + }, + mapper: action.AddedEventMapper, + }, w + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + queries := mock.NewMockQueries(ctrl) + queue := mock.NewMockQueue(ctrl) + f, a, w := tt.test(ctrl, queries, queue) + + event, err := a.mapper(a.event) + assert.NoError(t, err) + + stmt, err := newEventExecutionsHandler(queries, f).reduce(event) + if w.err != nil { + w.err(t, err) + return + } + assert.NoError(t, err) + + if w.noOperation { + assert.Nil(t, stmt.Execute) + return + } + err = stmt.Execute(nil, "") + if w.stmtErr != nil { + w.stmtErr(t, err) + return + } + assert.NoError(t, err) + }) + } +} + +func mockTarget() *query.ExecutionTarget { + return &query.ExecutionTarget{ + InstanceID: "instanceID", + ExecutionID: "executionID", + TargetID: "targetID", + TargetType: domain.TargetTypeWebhook, + Endpoint: "endpoint", + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "key", + } +} + +func mockTargets(count int) []*query.ExecutionTarget { + var targets []*query.ExecutionTarget + if count > 0 { + targets = make([]*query.ExecutionTarget, count) + for i := range targets { + targets[i] = mockTarget() + } + } + return targets +} + +func mockTargetsToBytes(targets []*query.ExecutionTarget) []byte { + data, _ := json.Marshal(targets) + return data +} + +func newEventExecutionsHandler(queries *mock.MockQueries, f fields) *eventHandler { + return &eventHandler{ + queue: f.queue, + query: queries, + } +} diff --git a/internal/execution/mock/queries.mock.go b/internal/execution/mock/queries.mock.go new file mode 100644 index 0000000000..ab7cf38a32 --- /dev/null +++ b/internal/execution/mock/queries.mock.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/internal/execution (interfaces: Queries) +// +// Generated by this command: +// +// mockgen -package mock -destination ./mock/queries.mock.go github.com/zitadel/zitadel/internal/execution Queries +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + authz "github.com/zitadel/zitadel/internal/api/authz" + query "github.com/zitadel/zitadel/internal/query" + gomock "go.uber.org/mock/gomock" +) + +// MockQueries is a mock of Queries interface. +type MockQueries struct { + ctrl *gomock.Controller + recorder *MockQueriesMockRecorder +} + +// MockQueriesMockRecorder is the mock recorder for MockQueries. +type MockQueriesMockRecorder struct { + mock *MockQueries +} + +// NewMockQueries creates a new mock instance. +func NewMockQueries(ctrl *gomock.Controller) *MockQueries { + mock := &MockQueries{ctrl: ctrl} + mock.recorder = &MockQueriesMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockQueries) EXPECT() *MockQueriesMockRecorder { + return m.recorder +} + +// InstanceByID mocks base method. +func (m *MockQueries) InstanceByID(arg0 context.Context, arg1 string) (authz.Instance, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InstanceByID", arg0, arg1) + ret0, _ := ret[0].(authz.Instance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InstanceByID indicates an expected call of InstanceByID. +func (mr *MockQueriesMockRecorder) InstanceByID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceByID", reflect.TypeOf((*MockQueries)(nil).InstanceByID), arg0, arg1) +} + +// TargetsByExecutionID mocks base method. +func (m *MockQueries) TargetsByExecutionID(arg0 context.Context, arg1 []string) ([]*query.ExecutionTarget, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TargetsByExecutionID", arg0, arg1) + ret0, _ := ret[0].([]*query.ExecutionTarget) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TargetsByExecutionID indicates an expected call of TargetsByExecutionID. +func (mr *MockQueriesMockRecorder) TargetsByExecutionID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TargetsByExecutionID", reflect.TypeOf((*MockQueries)(nil).TargetsByExecutionID), arg0, arg1) +} diff --git a/internal/execution/mock/queue.mock.go b/internal/execution/mock/queue.mock.go new file mode 100644 index 0000000000..c0e8d5fc7b --- /dev/null +++ b/internal/execution/mock/queue.mock.go @@ -0,0 +1,61 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/internal/execution (interfaces: Queue) +// +// Generated by this command: +// +// mockgen -package mock -destination ./mock/queue.mock.go github.com/zitadel/zitadel/internal/execution Queue +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + river "github.com/riverqueue/river" + queue "github.com/zitadel/zitadel/internal/queue" + gomock "go.uber.org/mock/gomock" +) + +// MockQueue is a mock of Queue interface. +type MockQueue struct { + ctrl *gomock.Controller + recorder *MockQueueMockRecorder +} + +// MockQueueMockRecorder is the mock recorder for MockQueue. +type MockQueueMockRecorder struct { + mock *MockQueue +} + +// NewMockQueue creates a new mock instance. +func NewMockQueue(ctrl *gomock.Controller) *MockQueue { + mock := &MockQueue{ctrl: ctrl} + mock.recorder = &MockQueueMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockQueue) EXPECT() *MockQueueMockRecorder { + return m.recorder +} + +// Insert mocks base method. +func (m *MockQueue) Insert(arg0 context.Context, arg1 river.JobArgs, arg2 ...queue.InsertOpt) error { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Insert", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockQueueMockRecorder) Insert(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockQueue)(nil).Insert), varargs...) +} diff --git a/internal/execution/projections.go b/internal/execution/projections.go new file mode 100644 index 0000000000..d16d7c6fca --- /dev/null +++ b/internal/execution/projections.go @@ -0,0 +1,36 @@ +package execution + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/queue" +) + +var ( + projections []*handler.Handler +) + +func Register( + ctx context.Context, + executionsCustomConfig projection.CustomConfig, + workerConfig WorkerConfig, + queries *query.Queries, + eventTypes []string, + queue *queue.Queue, +) { + queue.ShouldStart() + projections = []*handler.Handler{ + NewEventHandler(ctx, projection.ApplyCustomConfig(executionsCustomConfig), eventTypes, eventstore.AggregateTypeFromEventType, queries, queue), + } + queue.AddWorkers(NewWorker(workerConfig)) +} + +func Start(ctx context.Context) { + for _, projection := range projections { + projection.Start(ctx) + } +} diff --git a/internal/execution/target_test.go b/internal/execution/target_test.go new file mode 100644 index 0000000000..8df480219d --- /dev/null +++ b/internal/execution/target_test.go @@ -0,0 +1,85 @@ +package execution + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "reflect" + "time" +) + +type testServer struct { + server *httptest.Server + called bool +} + +func (s *testServer) URL() string { + return s.server.URL +} + +func (s *testServer) Close() { + s.server.Close() +} + +func (s *testServer) Called() bool { + return s.called +} + +func testServerCall( + reqBody interface{}, + sleep time.Duration, + statusCode int, + respBody interface{}, +) (string, func(), func() bool) { + server := &testServer{ + called: false, + } + + handler := func(w http.ResponseWriter, r *http.Request) { + server.called = true + if reqBody != nil { + data, err := json.Marshal(reqBody) + if err != nil { + http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError) + return + } + sentBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError) + return + } + if !reflect.DeepEqual(data, sentBody) { + http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError) + return + } + } + if statusCode != http.StatusOK { + http.Error(w, "error, statusCode", statusCode) + return + } + + time.Sleep(sleep) + + if respBody != nil { + w.Header().Set("Content-Type", "application/json") + resp, err := json.Marshal(respBody) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + if _, err := w.Write(resp); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } else { + if _, err := io.WriteString(w, "finished successfully"); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + } + + server.server = httptest.NewServer(http.HandlerFunc(handler)) + return server.URL(), server.Close, server.Called +} diff --git a/internal/execution/worker.go b/internal/execution/worker.go new file mode 100644 index 0000000000..105fa7d46e --- /dev/null +++ b/internal/execution/worker.go @@ -0,0 +1,90 @@ +package execution + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/riverqueue/river" + + "github.com/zitadel/zitadel/internal/query" + exec_repo "github.com/zitadel/zitadel/internal/repository/execution" +) + +type Worker struct { + river.WorkerDefaults[*exec_repo.Request] + + config WorkerConfig + now nowFunc +} + +// Timeout implements the Timeout-function of [river.Worker]. +// Maximum time a job can run before the context gets cancelled. +// The time can be shorter than the sum of target timeouts, this is expected behavior to not block the request indefinitely. +func (w *Worker) Timeout(*river.Job[*exec_repo.Request]) time.Duration { + return w.config.TransactionDuration +} + +// Work implements [river.Worker]. +func (w *Worker) Work(ctx context.Context, job *river.Job[*exec_repo.Request]) error { + ctx = ContextWithExecuter(ctx, job.Args.Aggregate) + + // if the event is too old, we can directly return as it will be removed anyway + if job.CreatedAt.Add(w.config.MaxTtl).Before(w.now()) { + return river.JobCancel(errors.New("event is too old")) + } + + targets, err := TargetsFromRequest(job.Args) + if err != nil { + // If we are not able to get the targets from the request, we can cancel the job, as we have nothing to call + return river.JobCancel(fmt.Errorf("unable to unmarshal targets because %w", err)) + } + + _, err = CallTargets(ctx, targets, exec_repo.ContextInfoFromRequest(job.Args)) + if err != nil { + // If there is an error returned from the targets, it means that the execution was interrupted + return river.JobCancel(fmt.Errorf("interruption during call of targets because %w", err)) + } + return nil +} + +// nowFunc makes [time.Now] mockable +type nowFunc func() time.Time + +type WorkerConfig struct { + Workers uint8 + TransactionDuration time.Duration + MaxTtl time.Duration +} + +func NewWorker( + config WorkerConfig, +) *Worker { + return &Worker{ + config: config, + now: time.Now, + } +} + +var _ river.Worker[*exec_repo.Request] = (*Worker)(nil) + +func (w *Worker) Register(workers *river.Workers, queues map[string]river.QueueConfig) { + river.AddWorker(workers, w) + queues[exec_repo.QueueName] = river.QueueConfig{ + MaxWorkers: int(w.config.Workers), + } +} + +func TargetsFromRequest(e *exec_repo.Request) ([]Target, error) { + var execTargets []*query.ExecutionTarget + if err := json.Unmarshal(e.TargetsData, &execTargets); err != nil { + return nil, err + } + targets := make([]Target, len(execTargets)) + for i, target := range execTargets { + targets[i] = target + } + return targets, nil +} diff --git a/internal/execution/worker_test.go b/internal/execution/worker_test.go new file mode 100644 index 0000000000..32f7879477 --- /dev/null +++ b/internal/execution/worker_test.go @@ -0,0 +1,288 @@ +package execution + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "testing" + "time" + + "github.com/riverqueue/river" + "github.com/riverqueue/river/rivertype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/execution/mock" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/repository/action" + exec_repo "github.com/zitadel/zitadel/internal/repository/execution" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type fields struct { + queries *mock.MockQueries + queue *mock.MockQueue +} +type fieldsWorker struct { + now nowFunc +} +type args struct { + event eventstore.Event + mapper func(event eventstore.Event) (eventstore.Event, error) +} +type argsWorker struct { + job *river.Job[*exec_repo.Request] +} +type want struct { + noOperation bool + err assert.ErrorAssertionFunc + stmtErr assert.ErrorAssertionFunc +} +type wantWorker struct { + targets []*query.ExecutionTarget + sendStatusCode int + err assert.ErrorAssertionFunc +} + +func newExecutionWorker(f fieldsWorker) *Worker { + return &Worker{ + config: WorkerConfig{ + Workers: 1, + TransactionDuration: 5 * time.Second, + MaxTtl: 5 * time.Minute, + }, + now: f.now, + } +} + +const ( + userID = "user1" + orgID = "orgID" + instanceID = "instanceID" + eventID = "eventID" + eventData = `{"name":"name","script":"name(){}","timeout":3000000000,"allowedToFail":true}` +) + +func Test_handleEventExecution(t *testing.T) { + testNow := time.Now + tests := []struct { + name string + test func() (fieldsWorker, argsWorker, wantWorker) + }{ + { + "max TTL", + func() (fieldsWorker, argsWorker, wantWorker) { + return fieldsWorker{ + now: testNow, + }, + argsWorker{ + job: &river.Job[*exec_repo.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now().Add(-1 * time.Hour), + }, + Args: &exec_repo.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + ID: eventID, + ResourceOwner: instanceID, + }, + Sequence: 1, + CreatedAt: time.Now().Add(-1 * time.Hour), + EventType: user.HumanInviteCodeAddedType, + UserID: userID, + EventData: []byte(eventData), + }, + }, + }, + wantWorker{ + targets: mockTargets(1), + sendStatusCode: http.StatusOK, + err: func(tt assert.TestingT, err error, i ...interface{}) bool { + return errors.Is(err, new(river.JobCancelError)) + }, + } + }, + }, + { + "none", + func() (fieldsWorker, argsWorker, wantWorker) { + return fieldsWorker{ + now: testNow, + }, + argsWorker{ + job: &river.Job[*exec_repo.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now(), + }, + Args: &exec_repo.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + ID: eventID, + ResourceOwner: instanceID, + }, + Sequence: 1, + CreatedAt: time.Now(), + EventType: user.HumanInviteCodeAddedType, + UserID: userID, + EventData: []byte(eventData), + }, + }, + }, + wantWorker{ + targets: mockTargets(0), + sendStatusCode: http.StatusOK, + err: nil, + } + }, + }, + { + "single", + func() (fieldsWorker, argsWorker, wantWorker) { + return fieldsWorker{ + now: testNow, + }, + argsWorker{ + job: &river.Job[*exec_repo.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now(), + }, + Args: &exec_repo.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + Type: action.AggregateType, + Version: action.AggregateVersion, + ID: eventID, + ResourceOwner: orgID, + }, + Sequence: 1, + CreatedAt: time.Now().UTC(), + EventType: action.AddedEventType, + UserID: userID, + EventData: []byte(eventData), + }, + }, + }, + wantWorker{ + targets: mockTargets(1), + sendStatusCode: http.StatusOK, + err: nil, + } + }, + }, + { + "single, failed 400", + func() (fieldsWorker, argsWorker, wantWorker) { + return fieldsWorker{ + now: testNow, + }, + argsWorker{ + job: &river.Job[*exec_repo.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now(), + }, + Args: &exec_repo.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + Type: action.AggregateType, + Version: action.AggregateVersion, + ID: eventID, + ResourceOwner: orgID, + }, + Sequence: 1, + CreatedAt: time.Now().UTC(), + EventType: action.AddedEventType, + UserID: userID, + EventData: []byte(eventData), + }, + }, + }, + wantWorker{ + targets: mockTargets(1), + sendStatusCode: http.StatusBadRequest, + err: func(tt assert.TestingT, err error, i ...interface{}) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "EXEC-dra6yamk98", "Errors.Execution.Failed")) + }, + } + }, + }, + { + "multiple", + func() (fieldsWorker, argsWorker, wantWorker) { + return fieldsWorker{ + now: testNow, + }, + argsWorker{ + job: &river.Job[*exec_repo.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now(), + }, + Args: &exec_repo.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + Type: action.AggregateType, + Version: action.AggregateVersion, + ID: eventID, + ResourceOwner: orgID, + }, + Sequence: 1, + CreatedAt: time.Now().UTC(), + EventType: action.AddedEventType, + UserID: userID, + EventData: []byte(eventData), + }, + }, + }, + wantWorker{ + targets: mockTargets(3), + sendStatusCode: http.StatusOK, + err: nil, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, a, w := tt.test() + + closeFuncs := make([]func(), len(w.targets)) + calledFuncs := make([]func() bool, len(w.targets)) + for i := range w.targets { + url, closeF, calledF := testServerCall( + exec_repo.ContextInfoFromRequest(a.job.Args), + time.Second, + w.sendStatusCode, + nil, + ) + w.targets[i].Endpoint = url + closeFuncs[i] = closeF + calledFuncs[i] = calledF + } + + data, err := json.Marshal(w.targets) + require.NoError(t, err) + a.job.Args.TargetsData = data + + err = newExecutionWorker(f).Work( + authz.WithInstanceID(context.Background(), instanceID), + a.job, + ) + + if w.err != nil { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + for _, closeF := range closeFuncs { + closeF() + } + for _, calledF := range calledFuncs { + assert.True(t, calledF()) + } + }) + } +} diff --git a/internal/feature/feature.go b/internal/feature/feature.go index 638917fd68..b5f5a901d4 100644 --- a/internal/feature/feature.go +++ b/internal/feature/feature.go @@ -15,7 +15,7 @@ const ( KeyLegacyIntrospection KeyUserSchema KeyTokenExchange - KeyActions + KeyActionsDeprecated KeyImprovedPerformance KeyWebKey KeyDebugOIDCParentError @@ -46,7 +46,6 @@ type Features struct { LegacyIntrospection bool `json:"legacy_introspection,omitempty"` UserSchema bool `json:"user_schema,omitempty"` TokenExchange bool `json:"token_exchange,omitempty"` - Actions bool `json:"actions,omitempty"` ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"` WebKey bool `json:"web_key,omitempty"` DebugOIDCParentError bool `json:"debug_oidc_parent_error,omitempty"` @@ -58,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 5a37b96270..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) { @@ -30,7 +30,7 @@ func _KeyNoOp() { _ = x[KeyLegacyIntrospection-(3)] _ = x[KeyUserSchema-(4)] _ = x[KeyTokenExchange-(5)] - _ = x[KeyActions-(6)] + _ = x[KeyActionsDeprecated-(6)] _ = x[KeyImprovedPerformance-(7)] _ = x[KeyWebKey-(8)] _ = x[KeyDebugOIDCParentError-(9)] @@ -42,7 +42,7 @@ func _KeyNoOp() { _ = x[KeyConsoleUseV2UserApi-(15)] } -var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2, KeyConsoleUseV2UserApi} +var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActionsDeprecated, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2, KeyConsoleUseV2UserApi} var _KeyNameToValueMap = map[string]Key{ _KeyName[0:11]: KeyUnspecified, @@ -57,26 +57,26 @@ var _KeyNameToValueMap = map[string]Key{ _KeyLowerName[81:92]: KeyUserSchema, _KeyName[92:106]: KeyTokenExchange, _KeyLowerName[92:106]: KeyTokenExchange, - _KeyName[106:113]: KeyActions, - _KeyLowerName[106:113]: KeyActions, - _KeyName[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/id/sonyflake.go b/internal/id/sonyflake.go index 609570436a..cc7086aa66 100644 --- a/internal/id/sonyflake.go +++ b/internal/id/sonyflake.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" "hash/fnv" - "io/ioutil" + "io" "net" "net/http" "os" @@ -73,7 +73,7 @@ func privateIPv4() (net.IP, error) { } } - //change: use "POD_IP" + // change: use "POD_IP" ip := net.ParseIP(os.Getenv("POD_IP")) if ip == nil { return nil, errors.New("no private ip address") @@ -140,7 +140,7 @@ func machineID() (uint16, error) { } logging.WithFields("errors", strings.Join(errors, ", ")).Panic("none of the enabled methods for identifying the machine succeeded") - //this return will never happen because of panic one line before + // this return will never happen because of panic one line before return 0, nil } @@ -200,7 +200,7 @@ func metadataWebhookID() (uint16, error) { if resp.StatusCode >= 400 && resp.StatusCode < 600 { return 0, fmt.Errorf("metadata endpoint returned an unsuccessful status code %d", resp.StatusCode) } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return 0, err } diff --git a/internal/idp/providers/azuread/azuread.go b/internal/idp/providers/azuread/azuread.go index 65f38ede5b..a15f793e37 100644 --- a/internal/idp/providers/azuread/azuread.go +++ b/internal/idp/providers/azuread/azuread.go @@ -152,6 +152,10 @@ func ensureMinimalScope(scopes []string) []string { return scopes } +func (p *Provider) User() idp.User { + return p.Provider.User() +} + // User represents the structure return on the userinfo endpoint and implements the [idp.User] interface // // AzureAD does not return an `email_verified` claim. diff --git a/internal/idp/providers/oauth/oauth2.go b/internal/idp/providers/oauth/oauth2.go index e9c627509a..a790c550f5 100644 --- a/internal/idp/providers/oauth/oauth2.go +++ b/internal/idp/providers/oauth/oauth2.go @@ -18,7 +18,7 @@ type Provider struct { options []rp.Option name string userEndpoint string - userMapper func() idp.User + user func() idp.User isLinkingAllowed bool isCreationAllowed bool isAutoCreation bool @@ -65,11 +65,11 @@ func WithRelyingPartyOption(option rp.Option) ProviderOpts { } // New creates a generic OAuth 2.0 provider -func New(config *oauth2.Config, name, userEndpoint string, userMapper func() idp.User, options ...ProviderOpts) (provider *Provider, err error) { +func New(config *oauth2.Config, name, userEndpoint string, user func() idp.User, options ...ProviderOpts) (provider *Provider, err error) { provider = &Provider{ name: name, userEndpoint: userEndpoint, - userMapper: userMapper, + user: user, generateVerifier: oauth2.GenerateVerifier, } for _, option := range options { @@ -137,3 +137,7 @@ func (p *Provider) IsAutoCreation() bool { func (p *Provider) IsAutoUpdate() bool { return p.isAutoUpdate } + +func (p *Provider) User() idp.User { + return p.user() +} diff --git a/internal/idp/providers/oauth/session.go b/internal/idp/providers/oauth/session.go index aca22234a2..247a7f8710 100644 --- a/internal/idp/providers/oauth/session.go +++ b/internal/idp/providers/oauth/session.go @@ -51,7 +51,7 @@ func (s *Session) PersistentParameters() map[string]any { // FetchUser implements the [idp.Session] interface. // It will execute an OAuth 2.0 code exchange if needed to retrieve the access token, // call the specified userEndpoint and map the received information into an [idp.User]. -func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { +func (s *Session) FetchUser(ctx context.Context) (_ idp.User, err error) { if s.Tokens == nil { if err = s.authorize(ctx); err != nil { return nil, err @@ -62,11 +62,11 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return nil, err } req.Header.Set("authorization", s.Tokens.TokenType+" "+s.Tokens.AccessToken) - mapper := s.Provider.userMapper() - if err := httphelper.HttpRequest(s.Provider.RelyingParty.HttpClient(), req, &mapper); err != nil { + user := s.Provider.User() + if err := httphelper.HttpRequest(s.Provider.RelyingParty.HttpClient(), req, &user); err != nil { return nil, err } - return mapper, nil + return user, nil } func (s *Session) authorize(ctx context.Context) (err error) { diff --git a/internal/integration/action.go b/internal/integration/action.go new file mode 100644 index 0000000000..e849b5c21c --- /dev/null +++ b/internal/integration/action.go @@ -0,0 +1,163 @@ +package integration + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "reflect" + "sync" + "time" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +type server struct { + server *httptest.Server + mu sync.Mutex + called int +} + +func (s *server) URL() string { + return s.server.URL +} + +func (s *server) Close() { + s.server.Close() +} + +func (s *server) Called() int { + s.mu.Lock() + called := s.called + s.mu.Unlock() + return called +} + +func (s *server) Increase() { + s.mu.Lock() + s.called++ + s.mu.Unlock() +} + +func (s *server) ResetCalled() { + s.mu.Lock() + s.called = 0 + s.mu.Unlock() +} + +func TestServerCall( + reqBody interface{}, + sleep time.Duration, + statusCode int, + respBody interface{}, +) (url string, closeF func(), calledF func() int, resetCalledF func()) { + server := &server{ + called: 0, + } + + handler := func(w http.ResponseWriter, r *http.Request) { + server.Increase() + if reqBody != nil { + data, err := json.Marshal(reqBody) + if err != nil { + http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError) + return + } + sentBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError) + return + } + if !reflect.DeepEqual(data, sentBody) { + http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError) + return + } + } + if statusCode != http.StatusOK { + http.Error(w, "error, statusCode", statusCode) + return + } + + time.Sleep(sleep) + + if respBody != nil { + w.Header().Set("Content-Type", "application/json") + resp, err := json.Marshal(respBody) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + if _, err := io.Writer.Write(w, resp); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } else { + if _, err := io.WriteString(w, "finished successfully"); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + } + + server.server = httptest.NewServer(http.HandlerFunc(handler)) + return server.URL(), server.Close, server.Called, server.ResetCalled +} + +func TestServerCallProto( + reqBody interface{}, + sleep time.Duration, + statusCode int, + respBody proto.Message, +) (url string, closeF func(), calledF func() int, resetCalledF func()) { + server := &server{ + called: 0, + } + + handler := func(w http.ResponseWriter, r *http.Request) { + server.Increase() + if reqBody != nil { + data, err := json.Marshal(reqBody) + if err != nil { + http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError) + return + } + sentBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError) + return + } + if !reflect.DeepEqual(data, sentBody) { + http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError) + return + } + } + if statusCode != http.StatusOK { + http.Error(w, "error, statusCode", statusCode) + return + } + + time.Sleep(sleep) + + if respBody != nil { + w.Header().Set("Content-Type", "application/json") + resp, err := protojson.Marshal(respBody) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + if _, err := io.Writer.Write(w, resp); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } else { + if _, err := io.WriteString(w, "finished successfully"); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + } + + server.server = httptest.NewServer(http.HandlerFunc(handler)) + return server.URL(), server.Close, server.Called, server.ResetCalled +} diff --git a/internal/integration/client.go b/internal/integration/client.go index a480a86ce0..e82a6bec55 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -3,6 +3,7 @@ package integration import ( "context" "fmt" + "sync" "testing" "time" @@ -18,6 +19,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration/scim" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" "github.com/zitadel/zitadel/pkg/grpc/admin" "github.com/zitadel/zitadel/pkg/grpc/auth" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" @@ -31,10 +33,8 @@ import ( oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" "github.com/zitadel/zitadel/pkg/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" - webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" "github.com/zitadel/zitadel/pkg/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" @@ -43,6 +43,7 @@ import ( user_pb "github.com/zitadel/zitadel/pkg/grpc/user" user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + webkey_v2beta "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) type Client struct { @@ -60,11 +61,11 @@ type Client struct { OIDCv2 oidc_pb.OIDCServiceClient OrgV2beta org_v2beta.OrganizationServiceClient OrgV2 org.OrganizationServiceClient - ActionV3Alpha action.ZITADELActionsClient + ActionV2beta action.ActionServiceClient FeatureV2beta feature_v2beta.FeatureServiceClient FeatureV2 feature.FeatureServiceClient UserSchemaV3 userschema_v3alpha.ZITADELUserSchemasClient - WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient + WebKeyV2Beta webkey_v2beta.WebKeyServiceClient IDPv2 idp_pb.IdentityProviderServiceClient UserV3Alpha user_v3alpha.ZITADELUsersClient SAMLv2 saml_pb.SAMLServiceClient @@ -93,11 +94,11 @@ func newClient(ctx context.Context, target string) (*Client, error) { OIDCv2: oidc_pb.NewOIDCServiceClient(cc), OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc), OrgV2: org.NewOrganizationServiceClient(cc), - ActionV3Alpha: action.NewZITADELActionsClient(cc), + ActionV2beta: action.NewActionServiceClient(cc), FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc), FeatureV2: feature.NewFeatureServiceClient(cc), UserSchemaV3: userschema_v3alpha.NewZITADELUserSchemasClient(cc), - WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc), + WebKeyV2Beta: webkey_v2beta.NewWebKeyServiceClient(cc), IDPv2: idp_pb.NewIdentityProviderServiceClient(cc), UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc), SAMLv2: saml_pb.NewSAMLServiceClient(cc), @@ -157,6 +158,7 @@ func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserRes }, }) logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetUserId()) return resp } @@ -181,6 +183,7 @@ func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHuman }, }) logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetUserId()) return resp } @@ -212,9 +215,26 @@ func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) * TotpSecret: gu.Ptr(secret), }) logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetUserId()) return resp } +// TriggerUserByID makes sure the user projection gets triggered after creation. +func (i *Instance) TriggerUserByID(ctx context.Context, users ...string) { + var wg sync.WaitGroup + wg.Add(len(users)) + for _, user := range users { + go func(user string) { + defer wg.Done() + _, err := i.Client.UserV2.GetUserByID(ctx, &user_v2.GetUserByIDRequest{ + UserId: user, + }) + logging.OnError(err).Warn("get user by ID for trigger failed") + }(user) + } + wg.Wait() +} + func (i *Instance) CreateOrganization(ctx context.Context, name, adminEmail string) *org.AddOrganizationResponse { resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ Name: name, @@ -238,6 +258,13 @@ func (i *Instance) CreateOrganization(ctx context.Context, name, adminEmail stri }, }) logging.OnError(err).Panic("create org") + + users := make([]string, len(resp.GetCreatedAdmins())) + for i, admin := range resp.GetCreatedAdmins() { + users[i] = admin.GetUserId() + } + i.TriggerUserByID(ctx, users...) + return resp } @@ -302,6 +329,7 @@ func (i *Instance) CreateHumanUserVerified(ctx context.Context, org, email, phon }, }) logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetUserId()) return resp } @@ -313,6 +341,7 @@ func (i *Instance) CreateMachineUser(ctx context.Context) *mgmt.AddMachineUserRe AccessTokenType: user_pb.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER, }) logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetUserId()) return resp } @@ -472,6 +501,26 @@ func (i *Instance) AddOrgGenericOAuthProvider(ctx context.Context, name string) return resp } +func (i *Instance) AddGenericOIDCProvider(ctx context.Context, name string) *admin.AddGenericOIDCProviderResponse { + resp, err := i.Client.Admin.AddGenericOIDCProvider(ctx, &admin.AddGenericOIDCProviderRequest{ + Name: name, + Issuer: "https://example.com", + ClientId: "clientID", + ClientSecret: "clientSecret", + Scopes: []string{"openid", "profile", "email"}, + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + IsIdTokenMapping: false, + }) + logging.OnError(err).Panic("create generic oidc idp") + return resp +} + func (i *Instance) AddSAMLProvider(ctx context.Context) string { resp, err := i.Client.Admin.AddSAMLProvider(ctx, &admin.AddSAMLProviderRequest{ Name: "saml-idp", @@ -526,6 +575,32 @@ func (i *Instance) AddSAMLPostProvider(ctx context.Context) string { return resp.GetId() } +func (i *Instance) AddLDAPProvider(ctx context.Context) string { + resp, err := i.Client.Admin.AddLDAPProvider(ctx, &admin.AddLDAPProviderRequest{ + Name: "ldap-idp-post", + Servers: []string{"https://localhost:8000"}, + StartTls: false, + BaseDn: "baseDn", + BindDn: "admin", + BindPassword: "admin", + UserBase: "dn", + UserObjectClasses: []string{"user"}, + UserFilters: []string{"(objectclass=*)"}, + Timeout: durationpb.New(10 * time.Second), + Attributes: &idp.LDAPAttributes{ + IdAttribute: "id", + }, + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }) + logging.OnError(err).Panic("create ldap idp") + return resp.GetId() +} + func (i *Instance) CreateIntent(ctx context.Context, idpID string) *user_v2.StartIdentityProviderIntentResponse { resp, err := i.Client.UserV2.StartIdentityProviderIntent(ctx, &user_v2.StartIdentityProviderIntentRequest{ IdpId: idpID, @@ -646,47 +721,52 @@ func (i *Instance) CreateTarget(ctx context.Context, t *testing.T, name, endpoin if name == "" { name = gofakeit.Name() } - reqTarget := &action.Target{ + req := &action.CreateTargetRequest{ Name: name, Endpoint: endpoint, - Timeout: durationpb.New(10 * time.Second), + Timeout: durationpb.New(5 * time.Second), } switch ty { case domain.TargetTypeWebhook: - reqTarget.TargetType = &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ + req.TargetType = &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ InterruptOnError: interrupt, }, } case domain.TargetTypeCall: - reqTarget.TargetType = &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ + req.TargetType = &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ InterruptOnError: interrupt, }, } case domain.TargetTypeAsync: - reqTarget.TargetType = &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, + req.TargetType = &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, } } - target, err := i.Client.ActionV3Alpha.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget}) + target, err := i.Client.ActionV2beta.CreateTarget(ctx, req) require.NoError(t, err) return target } +func (i *Instance) DeleteTarget(ctx context.Context, t *testing.T, id string) { + _, err := i.Client.ActionV2beta.DeleteTarget(ctx, &action.DeleteTargetRequest{ + Id: id, + }) + require.NoError(t, err) +} + func (i *Instance) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) { - _, err := i.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{ + _, err := i.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, }) require.NoError(t, err) } -func (i *Instance) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse { - target, err := i.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{ +func (i *Instance) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []string) *action.SetExecutionResponse { + target, err := i.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, - Execution: &action.Execution{ - Targets: targets, - }, + Targets: targets, }) require.NoError(t, err) return target diff --git a/internal/integration/config.go b/internal/integration/config.go index 5aea740752..0033e00104 100644 --- a/internal/integration/config.go +++ b/internal/integration/config.go @@ -20,10 +20,8 @@ type Config struct { WebAuthNName string } -var ( - //go:embed config/client.yaml - clientYAML []byte -) +//go:embed config/client.yaml +var clientYAML []byte var ( tmpDir string @@ -49,5 +47,6 @@ func init() { if err := loadedConfig.Log.SetLogger(); err != nil { panic(err) } - SystemToken = systemUserToken() + SystemToken = createSystemUserToken() + SystemUserWithNoPermissionsToken = createSystemUserWithNoPermissionsToken() } diff --git a/internal/integration/config/cockroach.yaml b/internal/integration/config/cockroach.yaml deleted file mode 100644 index 920e3cd6ec..0000000000 --- a/internal/integration/config/cockroach.yaml +++ /dev/null @@ -1,10 +0,0 @@ -Database: - cockroach: - Host: localhost - Port: 26257 - Database: zitadel - Options: "" - User: - Username: zitadel - Admin: - Username: root diff --git a/internal/integration/config/docker-compose.yaml b/internal/integration/config/docker-compose.yaml index 19c68ae405..8b54a22aec 100644 --- a/internal/integration/config/docker-compose.yaml +++ b/internal/integration/config/docker-compose.yaml @@ -1,11 +1,6 @@ version: '3.8' services: - cockroach: - extends: - file: '../../../e2e/config/localhost/docker-compose.yaml' - service: 'db' - postgres: restart: 'always' image: 'postgres:latest' diff --git a/internal/integration/config/postgres.yaml b/internal/integration/config/postgres.yaml index df1d08d3bc..904f973d56 100644 --- a/internal/integration/config/postgres.yaml +++ b/internal/integration/config/postgres.yaml @@ -1,16 +1,11 @@ Database: - EventPushConnRatio: 0.2 # 4 - ProjectionSpoolerConnRatio: 0.3 # 6 postgres: - Host: localhost - Port: 5432 - Database: zitadel MaxOpenConns: 20 MaxIdleConns: 20 MaxConnLifetime: 1h MaxConnIdleTime: 5m User: - Username: zitadel + Password: zitadel SSL: Mode: disable Admin: diff --git a/internal/integration/config/system-user-with-no-permissions.pem b/internal/integration/config/system-user-with-no-permissions.pem new file mode 100644 index 0000000000..801944ca75 --- /dev/null +++ b/internal/integration/config/system-user-with-no-permissions.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCMxYRfqb4fdnBl +ZmYweqUaZnWQv8RhWDYGifYGen00ozCFT2L6gGov4YCxRVe+l3aFQ79j5SJb1C+v +H68DJkyCTrhDpATqdjVuCu7CEEI//16Ivfmj3gbNdsp0IcDKVIAF0bN9kve5ofRX +CgU6DIx8GjLsXSooSniZnJ4d/Rnt69mpSsPkykUs3RpG2NSOn3WLAoVKh1q/kqeV +qf8eQ+KzuyD/R9QNAPiyB+ivAuOtVuvmIqojQYK5o8veTg/waBxdmzkim7eg8J7B +VDSjBeHagS5K9IJr/Q2VeO0rZOOeJfLlH9xlSrDvc3AIS/3HtkqI268kNkvpGz0I +sg61pUQtAgMBAAECggEAFzZrv1WPaQNAAex6fdR/fKS4Dqwcjxu7XuUpeUSB+GfP +dLAUR2/c8rPJ45FmaGJz9AIpoWiTe5Z33XYJRyjt1U/zQQ4fFGV1JoXtfHkvX3u1 +5DEFZQDT2NYViMRXFNYNvUfow9Rz/nuG/cJEfd+7W6x7SLANJ1MuY1Ao35OQjsOG +ftTtmEUppEIXyWL0PCeHQc83z8aJrP+p4hpjJOW2mui0NR2Hk456DGYXg8I8fcQD +ar7Ar7/A6thR0OmwG7tkkLjRiCjGwnkr19hCNLz+QAWB2o284T12zZueOqRuYQzu +KwNBZKJlClsPkhdZSPLL4RMFP6hJjKoP5mY0Zdzh8QKBgQDEPrM70aZQiweXHqoE +/vZry7tphGycoEAf6nwBBrZaRPpJdnEA61LBlJFv7C3s59uy6L7nHssTyVUJha9i +zFCWRQ0mHNrwxF5Ybd5p//hgblt3X53IV6vZBFF1+OrwRS/AKki3GynDc/oI++hu +PGHWmUF6lIi3uzWwOTqk6EGovQKBgQC3oqpUlpJ78e0zPjIr9ov61TtnPzAa883D +LL7fuNYP9zxIMoFZw++2bZfT5tbINflQdZnVVDNs5KiwtEu3oZJrsqXpQmzCl3j2 +KA9FTdVJQXc2lU90uYb76c5JZPownojbXFFOPQokBqfsYLSdfvNVHSQGjZ3C90wL +YZC0vA9YMQKBgQDCKSraD2YWoEeFO+CJityx8GNfVZbELETljvDbbxGyJDbhwh6y +AyHgxyZR7wHNN+UFkQN31d6kl/jbr/nDrVQ6KN2GjNwNhKu3oBSDGa9bcTRr2h1Y +32z2DTCvoPSJflptLSi+iVB7wd5rTxk7H+DJGt5O8nCGH+JRlX2xNN3pnQKBgDdA +u21eLM8cWNmNQj1WHoInfIsxSQEjEGtEYF4iWE5PfpTelWrz+IF0cjVxBHkTPGPI +LrQwdJS0LEmWxh2HgO3kv+TydpUKTHwMS6P3qlAzYXJL9K9TT1km3UnaFylf2h/e +pBwdY5q5YfdOlam50+9tKDTMkYZjMD9QaODooNlRAoGAOWow99WCATFtRrG+mGyl +UpwApgkZKT0nhkXUnLdNoQVeP0WHeQBSoOA24YnGBntvG/98Uj2rOwdCAYzTGepz +91bNqscrSOPdD3VN85GEl2DQKtxsRCKCdPKmYkvC/WMGhuzXSIp2U+ePgqEjEQO2 +Sn4xXZ1zwl+4cYHmDvzEQnA= +-----END PRIVATE KEY----- diff --git a/internal/integration/config/zitadel.yaml b/internal/integration/config/zitadel.yaml index e2642d9b8f..bb8d86376d 100644 --- a/internal/integration/config/zitadel.yaml +++ b/internal/integration/config/zitadel.yaml @@ -82,6 +82,13 @@ SystemAPIUsers: - "ORG_OWNER" - cypress: KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" + - system-user-with-no-permissions: + KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFqTVdFWDZtK0gzWndaV1ptTUhxbApHbVoxa0wvRVlWZzJCb24yQm5wOU5LTXdoVTlpK29CcUwrR0FzVVZYdnBkMmhVTy9ZK1VpVzlRdnJ4K3ZBeVpNCmdrNjRRNlFFNm5ZMWJncnV3aEJDUC85ZWlMMzVvOTRHelhiS2RDSEF5bFNBQmRHemZaTDN1YUgwVndvRk9neU0KZkJveTdGMHFLRXA0bVp5ZUhmMFo3ZXZacVVyRDVNcEZMTjBhUnRqVWpwOTFpd0tGU29kYXY1S25sYW4vSGtQaQpzN3NnLzBmVURRRDRzZ2ZvcndManJWYnI1aUtxSTBHQ3VhUEwzazRQOEdnY1haczVJcHUzb1BDZXdWUTBvd1hoCjJvRXVTdlNDYS8wTmxYanRLMlRqbmlYeTVSL2NaVXF3NzNOd0NFdjl4N1pLaU51dkpEWkw2UnM5Q0xJT3RhVkUKTFFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" + Memberships: + # MemberType System allows the user to access all APIs for all instances or organizations + - MemberType: IAM + Roles: + - "NO_ROLES" InitProjections: Enabled: true diff --git a/internal/integration/saml.go b/internal/integration/saml.go index 533b0ee515..28934ac421 100644 --- a/internal/integration/saml.go +++ b/internal/integration/saml.go @@ -17,6 +17,7 @@ import ( "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" "github.com/zitadel/logging" + "github.com/zitadel/saml/pkg/provider" http_util "github.com/zitadel/zitadel/internal/api/http" oidc_internal "github.com/zitadel/zitadel/internal/api/oidc" @@ -220,8 +221,15 @@ func (i *Instance) SuccessfulSAMLAuthRequest(ctx context.Context, userId, id str } func (i *Instance) GetSAMLIDPMetadata() (*saml.EntityDescriptor, error) { - idpEntityID := http_util.BuildHTTP(i.Domain, i.Config.Port, i.Config.Secure) + "/saml/v2/metadata" - resp, err := http.Get(idpEntityID) + issuer := i.Issuer() + "/saml/v2" + idpEntityID := issuer + "/metadata" + + req, err := http.NewRequestWithContext(provider.ContextWithIssuer(context.Background(), issuer), http.MethodGet, idpEntityID, nil) + if err != nil { + return nil, err + } + + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } diff --git a/internal/integration/sink/server.go b/internal/integration/sink/server.go index 2c79081e98..633ebf424f 100644 --- a/internal/integration/sink/server.go +++ b/internal/integration/sink/server.go @@ -27,6 +27,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/idp/providers/ldap" + "github.com/zitadel/zitadel/internal/idp/providers/oauth" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" "github.com/zitadel/zitadel/internal/idp/providers/saml" ) @@ -65,6 +66,24 @@ 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) { + u := url.URL{ + Scheme: "http", + Host: host, + Path: successfulIntentOIDCPath(), + } + resp, err := callIntent(u.String(), &SuccessfulIntentRequest{ + InstanceID: instanceID, + IDPID: idpID, + IDPUserID: idpUserID, + UserID: userID, + }) + if err != nil { + return "", "", time.Time{}, uint64(0), err + } + return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil +} + func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { u := url.URL{ Scheme: "http", @@ -119,6 +138,7 @@ func StartServer(commands *command.Commands) (close func()) { router.HandleFunc(rootPath(ch), fwd.receiveHandler) router.HandleFunc(subscribePath(ch), fwd.subscriptionHandler) router.HandleFunc(successfulIntentOAuthPath(), successfulIntentHandler(commands, createSuccessfulOAuthIntent)) + router.HandleFunc(successfulIntentOIDCPath(), successfulIntentHandler(commands, createSuccessfulOIDCIntent)) router.HandleFunc(successfulIntentSAMLPath(), successfulIntentHandler(commands, createSuccessfulSAMLIntent)) router.HandleFunc(successfulIntentLDAPPath(), successfulIntentHandler(commands, createSuccessfulLDAPIntent)) } @@ -159,6 +179,10 @@ func successfulIntentOAuthPath() string { return path.Join(successfulIntentPath(), "/", "oauth") } +func successfulIntentOIDCPath() string { + return path.Join(successfulIntentPath(), "/", "oidc") +} + func successfulIntentSAMLPath() string { return path.Join(successfulIntentPath(), "/", "saml") } @@ -334,6 +358,41 @@ func createIntent(ctx context.Context, cmd *command.Commands, instanceID, idpID } func createSuccessfulOAuthIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) { + intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID) + if err != nil { + return nil, err + } + writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID) + if err != nil { + return nil, err + } + idAttribute := "id" + idpUser := oauth.NewUserMapper(idAttribute) + idpUser.RawInfo = map[string]interface{}{ + idAttribute: req.IDPUserID, + "preferred_username": "username", + } + idpSession := &oauth.Session{ + Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ + Token: &oauth2.Token{ + AccessToken: "accessToken", + }, + IDToken: "idToken", + }, + } + token, err := cmd.SucceedIDPIntent(ctx, writeModel, idpUser, idpSession, req.UserID) + if err != nil { + return nil, err + } + return &SuccessfulIntentResponse{ + intentID, + token, + writeModel.ChangeDate, + writeModel.ProcessedSequence, + }, nil +} + +func createSuccessfulOIDCIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) { intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID) writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID) idpUser := openid.NewUser( diff --git a/internal/integration/system.go b/internal/integration/system.go index a9673a40ae..badc3db355 100644 --- a/internal/integration/system.go +++ b/internal/integration/system.go @@ -17,13 +17,16 @@ import ( var ( //go:embed config/system-user-key.pem systemUserKey []byte + //go:embed config/system-user-with-no-permissions.pem + systemUserWithNoPermissions []byte ) var ( // SystemClient creates a system connection once and reuses it on every use. // Each client call automatically gets the authorization context for the system user. - SystemClient = sync.OnceValue[system.SystemServiceClient](systemClient) - SystemToken string + SystemClient = sync.OnceValue[system.SystemServiceClient](systemClient) + SystemToken string + SystemUserWithNoPermissionsToken string ) func systemClient() system.SystemServiceClient { @@ -40,7 +43,7 @@ func systemClient() system.SystemServiceClient { return system.NewSystemServiceClient(cc) } -func systemUserToken() string { +func createSystemUserToken() string { const ISSUER = "tester" audience := http_util.BuildOrigin(loadedConfig.Host(), loadedConfig.Secure) signer, err := client.NewSignerFromPrivateKeyByte(systemUserKey, "") @@ -54,6 +57,24 @@ func systemUserToken() string { return token } +func createSystemUserWithNoPermissionsToken() string { + const ISSUER = "system-user-with-no-permissions" + audience := http_util.BuildOrigin(loadedConfig.Host(), loadedConfig.Secure) + signer, err := client.NewSignerFromPrivateKeyByte(systemUserWithNoPermissions, "") + if err != nil { + panic(err) + } + token, err := client.SignedJWTProfileAssertion(ISSUER, []string{audience}, time.Hour, signer) + if err != nil { + panic(err) + } + return token +} + func WithSystemAuthorization(ctx context.Context) context.Context { return WithAuthorizationToken(ctx, SystemToken) } + +func WithSystemUserWithNoPermissionsAuthorization(ctx context.Context) context.Context { + return WithAuthorizationToken(ctx, SystemUserWithNoPermissionsToken) +} diff --git a/internal/notification/handlers/notification_worker.go b/internal/notification/handlers/notification_worker.go index e2f1d58153..fa082bc345 100644 --- a/internal/notification/handlers/notification_worker.go +++ b/internal/notification/handlers/notification_worker.go @@ -13,14 +13,12 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/notification/channels" "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/notification/types" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/queue" "github.com/zitadel/zitadel/internal/repository/notification" ) @@ -34,13 +32,13 @@ type NotificationWorker struct { commands Commands queries *NotificationQueries - es *eventstore.Eventstore - client *database.DB channels types.ChannelChains config WorkerConfig now nowFunc } +// Timeout implements the Timeout-function of [river.Worker]. +// Maximum time a job can run before the context gets cancelled. func (w *NotificationWorker) Timeout(*river.Job[*notification.Request]) time.Duration { return w.config.TransactionDuration } @@ -106,24 +104,15 @@ func NewNotificationWorker( config WorkerConfig, commands Commands, queries *NotificationQueries, - es *eventstore.Eventstore, - client *database.DB, channels types.ChannelChains, - queue *queue.Queue, ) *NotificationWorker { - w := &NotificationWorker{ + return &NotificationWorker{ config: config, commands: commands, queries: queries, - es: es, - client: client, channels: channels, now: time.Now, } - if !config.LegacyEnabled { - queue.AddWorkers(w) - } - return w } var _ river.Worker[*notification.Request] = (*NotificationWorker)(nil) diff --git a/internal/notification/projections.go b/internal/notification/projections.go index 6a0296f3bf..9b6b975fa1 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -2,11 +2,14 @@ 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/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/notification/handlers" @@ -18,7 +21,6 @@ import ( var ( projections []*handler.Handler - worker *handlers.NotificationWorker ) func Register( @@ -35,7 +37,6 @@ func Register( otpEmailTmpl, fileSystemPath string, userEncryption, smtpEncryption, smsEncryption, keysEncryptionAlg crypto.EncryptionAlgorithm, tokenLifetime time.Duration, - client *database.DB, queue *queue.Queue, ) { if !notificationWorkerConfig.LegacyEnabled { @@ -59,7 +60,9 @@ func Register( if telemetryCfg.Enabled { projections = append(projections, handlers.NewTelemetryPusher(ctx, telemetryCfg, projection.ApplyCustomConfig(telemetryHandlerCustomConfig), commands, q, c)) } - worker = handlers.NewNotificationWorker(notificationWorkerConfig, commands, q, es, client, c, queue) + if !notificationWorkerConfig.LegacyEnabled { + queue.AddWorkers(handlers.NewNotificationWorker(notificationWorkerConfig, commands, q, c)) + } } func Start(ctx context.Context) { @@ -69,11 +72,13 @@ func Start(ctx context.Context) { } 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/notification/static/i18n/ro.yaml b/internal/notification/static/i18n/ro.yaml new file mode 100644 index 0000000000..f3c635a817 --- /dev/null +++ b/internal/notification/static/i18n/ro.yaml @@ -0,0 +1,68 @@ +InitCode: + Title: Inițializare Utilizator + PreHeader: Inițializare Utilizator + Subject: Inițializare Utilizator + Greeting: Bună ziua, {{.DisplayName}}, + Text: Acest utilizator a fost creat. Folosiți numele de utilizator {{.PreferredLoginName}} pentru a vă autentifica. Vă rugăm să dați clic pe butonul de mai jos pentru a finaliza procesul de inițializare. (Cod {{.Code}}) Dacă nu ați solicitat acest e-mail, vă rugăm să îl ignorați. + ButtonText: Finalizare inițializare +PasswordReset: + Title: Resetare parolă + PreHeader: Resetare parolă + Subject: Resetare parolă + Greeting: Bună ziua, {{.DisplayName}}, + Text: Am primit o cerere de resetare a parolei. Vă rugăm să folosiți butonul de mai jos pentru a vă reseta parola. (Cod {{.Code}}) Dacă nu ați solicitat acest e-mail, vă rugăm să îl ignorați. + ButtonText: Resetare parolă +VerifyEmail: + Title: Verificare e-mail + PreHeader: Verificare e-mail + Subject: Verificare e-mail + Greeting: Bună ziua, {{.DisplayName}}, + Text: A fost adăugat un e-mail nou. Vă rugăm să folosiți butonul de mai jos pentru a vă verifica e-mailul. (Cod {{.Code}}) Dacă nu ați adăugat un e-mail nou, vă rugăm să ignorați acest e-mail. + ButtonText: Verificare e-mail +VerifyPhone: + Title: Verificare telefon + PreHeader: Verificare telefon + Subject: Verificare telefon + Greeting: Bună ziua, {{.DisplayName}}, + Text: "A fost adăugat un număr de telefon nou. Vă rugăm să folosiți următorul cod pentru a-l verifica: {{.Code}}" + ButtonText: Verificare telefon +VerifyEmailOTP: + Title: Verificare parolă unică + PreHeader: Verificare parolă unică + Subject: Verificare parolă unică + Greeting: Bună ziua, {{.DisplayName}}, + Text: Vă rugăm să folosiți parola unică {{.OTP}} pentru a vă autentifica în următoarele cinci minute sau dați clic pe butonul "Autentificare". + ButtonText: Autentificare +VerifySMSOTP: + Text: >- + {{.OTP}} este parola dvs. unică pentru {{ .Domain }}. Folosiți-o în următoarele {{.Expiry}}. + + @{{.Domain}} #{{.OTP}} +DomainClaimed: + Title: Domeniul a fost revendicat + PreHeader: Schimbare e-mail / nume de utilizator + Subject: Domeniul a fost revendicat + Greeting: Bună ziua, {{.DisplayName}}, + Text: Domeniul {{.Domain}} a fost revendicat de o organizație. Utilizatorul dvs. actual {{.Username}} nu face parte din această organizație. Prin urmare, va trebui să vă schimbați e-mailul când vă autentificați. Am creat un nume de utilizator temporar ({{.TempUsername}}) pentru această autentificare. + ButtonText: Autentificare +PasswordlessRegistration: + Title: Adăugare autentificare fără parolă + PreHeader: Adăugare autentificare fără parolă + Subject: Adăugare autentificare fără parolă + Greeting: Bună ziua, {{.DisplayName}}, + Text: Am primit o cerere de adăugare a unui token pentru autentificare fără parolă. Vă rugăm să folosiți butonul de mai jos pentru a adăuga token-ul sau dispozitivul pentru autentificare fără parolă. + ButtonText: Adăugare autentificare fără parolă +PasswordChange: + Title: Parola utilizatorului a fost schimbată + PreHeader: Schimbare parolă + Subject: Parola utilizatorului a fost schimbată + Greeting: Bună ziua, {{.DisplayName}}, + Text: Parola utilizatorului dvs. a fost schimbată. Dacă această modificare nu a fost făcută de dvs., vă recomandăm să vă resetați imediat parola. + ButtonText: Autentificare +InviteUser: + Title: Invitație la {{.ApplicationName}} + PreHeader: Invitație la {{.ApplicationName}} + Subject: Invitație la {{.ApplicationName}} + Greeting: Bună ziua, {{.DisplayName}}, + Text: Utilizatorul dvs. a fost invitat la {{.ApplicationName}}. Vă rugăm să dați clic pe butonul de mai jos pentru a finaliza procesul de invitație. Dacă nu ați solicitat acest e-mail, vă rugăm să îl ignorați. + ButtonText: Acceptare invitație diff --git a/internal/notification/templates/template.go b/internal/notification/templates/template.go index 98209366c2..734d7052df 100644 --- a/internal/notification/templates/template.go +++ b/internal/notification/templates/template.go @@ -3,7 +3,7 @@ package templates import ( "bytes" "html/template" - "io/ioutil" + "io" "net/http" ) @@ -51,7 +51,7 @@ func readFile(dir http.FileSystem, fileName string) (*template.Template, error) return nil, err } defer f.Close() - content, err := ioutil.ReadAll(f) + content, err := io.ReadAll(f) if err != nil { return nil, err } @@ -68,7 +68,7 @@ func readFileFromDatabase(dir http.FileSystem, fileName string) (*template.Templ return nil, err } defer f.Close() - content, err := ioutil.ReadAll(f) + content, err := io.ReadAll(f) if err != nil { return nil, err } diff --git a/internal/query/action.go b/internal/query/action.go index 30ded403d1..45017572e2 100644 --- a/internal/query/action.go +++ b/internal/query/action.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -118,7 +117,7 @@ func (q *Queries) SearchActions(ctx context.Context, queries *ActionSearchQuerie ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareActionsQuery(ctx, q.client) + query, scan := prepareActionsQuery() eq := sq.Eq{ ActionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -146,7 +145,7 @@ func (q *Queries) GetActionByID(ctx context.Context, id string, orgID string, wi ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareActionQuery(ctx, q.client) + stmt, scan := prepareActionQuery() eq := sq.Eq{ ActionColumnID.identifier(): id, ActionColumnResourceOwner.identifier(): orgID, @@ -183,7 +182,7 @@ func NewActionIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(ActionColumnID, id, TextEquals) } -func prepareActionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Actions, error)) { +func prepareActionsQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*Actions, error)) { return sq.Select( ActionColumnID.identifier(), ActionColumnCreationDate.identifier(), @@ -196,7 +195,7 @@ func prepareActionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil ActionColumnTimeout.identifier(), ActionColumnAllowedToFail.identifier(), countColumn.identifier(), - ).From(actionTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(actionTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Actions, error) { actions := make([]*Action, 0) @@ -235,7 +234,7 @@ func prepareActionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } } -func prepareActionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Action, error)) { +func prepareActionQuery() (sq.SelectBuilder, func(row *sql.Row) (*Action, error)) { return sq.Select( ActionColumnID.identifier(), ActionColumnCreationDate.identifier(), @@ -247,7 +246,7 @@ func prepareActionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuild ActionColumnScript.identifier(), ActionColumnTimeout.identifier(), ActionColumnAllowedToFail.identifier(), - ).From(actionTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(actionTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Action, error) { action := new(Action) diff --git a/internal/query/action_flow.go b/internal/query/action_flow.go index c5263d6c43..6011b3f6e1 100644 --- a/internal/query/action_flow.go +++ b/internal/query/action_flow.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -67,7 +66,7 @@ func (q *Queries) GetFlow(ctx context.Context, flowType domain.FlowType, orgID s ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareFlowQuery(ctx, q.client, flowType) + query, scan := prepareFlowQuery(flowType) eq := sq.Eq{ FlowsTriggersColumnFlowType.identifier(): flowType, FlowsTriggersColumnResourceOwner.identifier(): orgID, @@ -89,7 +88,7 @@ func (q *Queries) GetActiveActionsByFlowAndTriggerType(ctx context.Context, flow ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareTriggerActionsQuery(ctx, q.client) + stmt, scan := prepareTriggerActionsQuery() eq := sq.Eq{ FlowsTriggersColumnFlowType.identifier(): flowType, FlowsTriggersColumnTriggerType.identifier(): triggerType, @@ -113,7 +112,7 @@ func (q *Queries) GetFlowTypesOfActionID(ctx context.Context, actionID string) ( ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareFlowTypesQuery(ctx, q.client) + stmt, scan := prepareFlowTypesQuery() eq := sq.Eq{ FlowsTriggersColumnActionID.identifier(): actionID, FlowsTriggersColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -130,11 +129,11 @@ func (q *Queries) GetFlowTypesOfActionID(ctx context.Context, actionID string) ( return types, err } -func prepareFlowTypesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) ([]domain.FlowType, error)) { +func prepareFlowTypesQuery() (sq.SelectBuilder, func(*sql.Rows) ([]domain.FlowType, error)) { return sq.Select( FlowsTriggersColumnFlowType.identifier(), ). - From(flowsTriggersTable.identifier() + db.Timetravel(call.Took(ctx))). + From(flowsTriggersTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) ([]domain.FlowType, error) { types := []domain.FlowType{} @@ -153,7 +152,7 @@ func prepareFlowTypesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } -func prepareTriggerActionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) ([]*Action, error)) { +func prepareTriggerActionsQuery() (sq.SelectBuilder, func(*sql.Rows) ([]*Action, error)) { return sq.Select( ActionColumnID.identifier(), ActionColumnCreationDate.identifier(), @@ -167,7 +166,7 @@ func prepareTriggerActionsQuery(ctx context.Context, db prepareDatabase) (sq.Sel ActionColumnTimeout.identifier(), ). From(flowsTriggersTable.name). - LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID)). OrderBy(FlowsTriggersColumnTriggerSequence.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) ([]*Action, error) { @@ -200,7 +199,7 @@ func prepareTriggerActionsQuery(ctx context.Context, db prepareDatabase) (sq.Sel } } -func prepareFlowQuery(ctx context.Context, db prepareDatabase, flowType domain.FlowType) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { +func prepareFlowQuery(flowType domain.FlowType) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { return sq.Select( ActionColumnID.identifier(), ActionColumnCreationDate.identifier(), @@ -220,7 +219,7 @@ func prepareFlowQuery(ctx context.Context, db prepareDatabase, flowType domain.F FlowsTriggersColumnResourceOwner.identifier(), ). From(flowsTriggersTable.name). - LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID)). OrderBy(FlowsTriggersColumnTriggerSequence.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Flow, error) { diff --git a/internal/query/action_flow_test.go b/internal/query/action_flow_test.go index af0db27278..7447313064 100644 --- a/internal/query/action_flow_test.go +++ b/internal/query/action_flow_test.go @@ -1,7 +1,6 @@ package query import ( - "context" "database/sql" "database/sql/driver" "errors" @@ -34,7 +33,6 @@ var ( ` projections.flow_triggers3.resource_owner` + ` FROM projections.flow_triggers3` + ` LEFT JOIN projections.actions3 ON projections.flow_triggers3.action_id = projections.actions3.id AND projections.flow_triggers3.instance_id = projections.actions3.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ORDER BY projections.flow_triggers3.trigger_sequence` prepareFlowCols = []string{ "id", @@ -68,7 +66,6 @@ var ( ` projections.actions3.timeout` + ` FROM projections.flow_triggers3` + ` LEFT JOIN projections.actions3 ON projections.flow_triggers3.action_id = projections.actions3.id AND projections.flow_triggers3.instance_id = projections.actions3.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ORDER BY projections.flow_triggers3.trigger_sequence` prepareTriggerActionCols = []string{ @@ -86,7 +83,6 @@ var ( prepareFlowTypeStmt = `SELECT projections.flow_triggers3.flow_type` + ` FROM projections.flow_triggers3` - // ` AS OF SYSTEM TIME '-1 ms'` prepareFlowTypeCols = []string{ "flow_type", @@ -106,8 +102,8 @@ func Test_FlowPrepares(t *testing.T) { }{ { name: "prepareFlowQuery no result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { - return prepareFlowQuery(ctx, db, domain.FlowTypeExternalAuthentication) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { + return prepareFlowQuery(domain.FlowTypeExternalAuthentication) }, want: want{ sqlExpectations: mockQueries( @@ -123,8 +119,8 @@ func Test_FlowPrepares(t *testing.T) { }, { name: "prepareFlowQuery one action", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { - return prepareFlowQuery(ctx, db, domain.FlowTypeExternalAuthentication) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { + return prepareFlowQuery(domain.FlowTypeExternalAuthentication) }, want: want{ sqlExpectations: mockQueries( @@ -177,8 +173,8 @@ func Test_FlowPrepares(t *testing.T) { }, { name: "prepareFlowQuery multiple actions", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { - return prepareFlowQuery(ctx, db, domain.FlowTypeExternalAuthentication) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { + return prepareFlowQuery(domain.FlowTypeExternalAuthentication) }, want: want{ sqlExpectations: mockQueries( @@ -263,8 +259,8 @@ func Test_FlowPrepares(t *testing.T) { }, { name: "prepareFlowQuery no action", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { - return prepareFlowQuery(ctx, db, domain.FlowTypeExternalAuthentication) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { + return prepareFlowQuery(domain.FlowTypeExternalAuthentication) }, want: want{ sqlExpectations: mockQueries( @@ -302,8 +298,8 @@ func Test_FlowPrepares(t *testing.T) { }, { name: "prepareFlowQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { - return prepareFlowQuery(ctx, db, domain.FlowTypeExternalAuthentication) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { + return prepareFlowQuery(domain.FlowTypeExternalAuthentication) }, want: want{ sqlExpectations: mockQueryErr( @@ -520,7 +516,7 @@ func Test_FlowPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/action_test.go b/internal/query/action_test.go index f6ba5be4b9..e5cad0e269 100644 --- a/internal/query/action_test.go +++ b/internal/query/action_test.go @@ -26,7 +26,6 @@ var ( ` projections.actions3.allowed_to_fail,` + ` COUNT(*) OVER ()` + ` FROM projections.actions3` - // ` AS OF SYSTEM TIME '-1 ms'` prepareActionsCols = []string{ "id", "creation_date", @@ -52,7 +51,6 @@ var ( ` projections.actions3.timeout,` + ` projections.actions3.allowed_to_fail` + ` FROM projections.actions3` - // ` AS OF SYSTEM TIME '-1 ms'` prepareActionCols = []string{ "id", "creation_date", @@ -289,7 +287,7 @@ func Test_ActionPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/app.go b/internal/query/app.go index fafbbe72d9..5fed1e3ced 100644 --- a/internal/query/app.go +++ b/internal/query/app.go @@ -11,7 +11,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -290,7 +289,7 @@ func (q *Queries) AppByProjectAndAppID(ctx context.Context, shouldTriggerBulk bo traceSpan.EndWithError(err) } - stmt, scan := prepareAppQuery(ctx, q.client, false) + stmt, scan := prepareAppQuery(false) eq := sq.Eq{ AppColumnID.identifier(): appID, AppColumnProjectID.identifier(): projectID, @@ -312,7 +311,7 @@ func (q *Queries) AppByID(ctx context.Context, appID string, activeOnly bool) (a ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareAppQuery(ctx, q.client, activeOnly) + stmt, scan := prepareAppQuery(activeOnly) eq := sq.Eq{ AppColumnID.identifier(): appID, AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -338,7 +337,7 @@ func (q *Queries) ProjectByClientID(ctx context.Context, appID string) (project ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareProjectByAppQuery(ctx, q.client) + stmt, scan := prepareProjectByAppQuery() eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} query, args, err := stmt.Where(sq.And{ eq, @@ -434,7 +433,7 @@ func (q *Queries) ProjectIDFromClientID(ctx context.Context, appID string) (id s ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareProjectIDByAppQuery(ctx, q.client) + stmt, scan := prepareProjectIDByAppQuery() eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} where := sq.And{ eq, @@ -460,7 +459,7 @@ func (q *Queries) ProjectByOIDCClientID(ctx context.Context, id string) (project ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareProjectByOIDCAppQuery(ctx, q.client) + stmt, scan := prepareProjectByOIDCAppQuery() eq := sq.Eq{ AppOIDCConfigColumnClientID.identifier(): id, AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -502,7 +501,7 @@ func (q *Queries) AppByClientID(ctx context.Context, clientID string) (app *App, ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareAppQuery(ctx, q.client, true) + stmt, scan := prepareAppQuery(true) eq := sq.Eq{ AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), AppColumnState.identifier(): domain.AppStateActive, @@ -531,7 +530,7 @@ func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, wit ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareAppsQuery(ctx, q.client) + query, scan := prepareAppsQuery() eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -560,7 +559,7 @@ func (q *Queries) SearchClientIDs(ctx context.Context, queries *AppSearchQueries traceSpan.EndWithError(err) } - query, scan := prepareClientIDsQuery(ctx, q.client) + query, scan := prepareClientIDsQuery() eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -581,7 +580,7 @@ func (q *Queries) OIDCClientLoginVersion(ctx context.Context, clientID string) ( ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginVersionByOIDCClientID(ctx, q.client) + query, scan := prepareLoginVersionByOIDCClientID() eq := sq.Eq{ AppOIDCConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), AppOIDCConfigColumnClientID.identifier(): clientID, @@ -605,7 +604,7 @@ func (q *Queries) SAMLAppLoginVersion(ctx context.Context, appID string) (loginV ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginVersionBySAMLAppID(ctx, q.client) + query, scan := prepareLoginVersionBySAMLAppID() eq := sq.Eq{ AppSAMLConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), AppSAMLConfigColumnAppID.identifier(): appID, @@ -633,7 +632,7 @@ func NewAppProjectIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(AppColumnProjectID, id, TextEquals) } -func prepareAppQuery(ctx context.Context, db prepareDatabase, activeOnly bool) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { +func prepareAppQuery(activeOnly bool) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { query := sq.Select( AppColumnID.identifier(), AppColumnName.identifier(), @@ -684,13 +683,13 @@ func prepareAppQuery(ctx context.Context, db prepareDatabase, activeOnly bool) ( LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID)). LeftJoin(join(ProjectColumnID, AppColumnProjectID)). - LeftJoin(join(OrgColumnID, AppColumnResourceOwner) + db.Timetravel(call.Took(ctx))), + LeftJoin(join(OrgColumnID, AppColumnResourceOwner)), scanApp } return query. LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). - LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID) + db.Timetravel(call.Took(ctx))), + LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID)), scanApp } @@ -845,13 +844,13 @@ func prepareOIDCAppQuery() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { } } -func prepareProjectIDByAppQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (projectID string, err error)) { +func prepareProjectIDByAppQuery() (sq.SelectBuilder, func(*sql.Row) (projectID string, err error)) { return sq.Select( AppColumnProjectID.identifier(), ).From(appsTable.identifier()). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). - LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (projectID string, err error) { err = row.Scan( &projectID, @@ -868,7 +867,7 @@ func prepareProjectIDByAppQuery(ctx context.Context, db prepareDatabase) (sq.Sel } } -func prepareProjectByOIDCAppQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { +func prepareProjectByOIDCAppQuery() (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { return sq.Select( ProjectColumnID.identifier(), ProjectColumnCreationDate.identifier(), @@ -910,7 +909,7 @@ func prepareProjectByOIDCAppQuery(ctx context.Context, db prepareDatabase) (sq.S } } -func prepareProjectByAppQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { +func prepareProjectByAppQuery() (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { return sq.Select( ProjectColumnID.identifier(), ProjectColumnCreationDate.identifier(), @@ -927,7 +926,7 @@ func prepareProjectByAppQuery(ctx context.Context, db prepareDatabase) (sq.Selec Join(join(AppColumnProjectID, ProjectColumnID)). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). - LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Project, error) { p := new(Project) @@ -954,7 +953,7 @@ func prepareProjectByAppQuery(ctx context.Context, db prepareDatabase) (sq.Selec } } -func prepareAppsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Apps, error)) { +func prepareAppsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Apps, error)) { return sq.Select( AppColumnID.identifier(), AppColumnName.identifier(), @@ -1000,7 +999,7 @@ func prepareAppsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder ).From(appsTable.identifier()). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). - LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Rows) (*Apps, error) { apps := &Apps{Apps: []*App{}} @@ -1072,13 +1071,13 @@ func prepareAppsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder } } -func prepareClientIDsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) ([]string, error)) { +func prepareClientIDsQuery() (sq.SelectBuilder, func(*sql.Rows) ([]string, error)) { return sq.Select( AppAPIConfigColumnClientID.identifier(), AppOIDCConfigColumnClientID.identifier(), ).From(appsTable.identifier()). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). - LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) ([]string, error) { ids := database.TextArray[string]{} @@ -1102,7 +1101,7 @@ func prepareClientIDsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } } -func prepareLoginVersionByOIDCClientID(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (domain.LoginVersion, error)) { +func prepareLoginVersionByOIDCClientID() (sq.SelectBuilder, func(*sql.Row) (domain.LoginVersion, error)) { return sq.Select( AppOIDCConfigColumnLoginVersion.identifier(), ).From(appOIDCConfigsTable.identifier()). @@ -1117,7 +1116,7 @@ func prepareLoginVersionByOIDCClientID(ctx context.Context, db prepareDatabase) } } -func prepareLoginVersionBySAMLAppID(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (domain.LoginVersion, error)) { +func prepareLoginVersionBySAMLAppID() (sq.SelectBuilder, func(*sql.Row) (domain.LoginVersion, error)) { return sq.Select( AppSAMLConfigColumnLoginVersion.identifier(), ).From(appSAMLConfigsTable.identifier()). diff --git a/internal/query/app_test.go b/internal/query/app_test.go index dbbcaef47c..c24060a60c 100644 --- a/internal/query/app_test.go +++ b/internal/query/app_test.go @@ -1,7 +1,6 @@ package query import ( - "context" "database/sql" "database/sql/driver" "errors" @@ -111,20 +110,17 @@ var ( ` FROM projections.apps7` + ` LEFT JOIN projections.apps7_api_configs ON projections.apps7.id = projections.apps7_api_configs.app_id AND projections.apps7.instance_id = projections.apps7_api_configs.instance_id` + ` LEFT JOIN projections.apps7_oidc_configs ON projections.apps7.id = projections.apps7_oidc_configs.app_id AND projections.apps7.instance_id = projections.apps7_oidc_configs.instance_id` + - ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id`) expectedAppIDsQuery = regexp.QuoteMeta(`SELECT projections.apps7_api_configs.client_id,` + ` projections.apps7_oidc_configs.client_id` + ` FROM projections.apps7` + ` LEFT JOIN projections.apps7_api_configs ON projections.apps7.id = projections.apps7_api_configs.app_id AND projections.apps7.instance_id = projections.apps7_api_configs.instance_id` + - ` LEFT JOIN projections.apps7_oidc_configs ON projections.apps7.id = projections.apps7_oidc_configs.app_id AND projections.apps7.instance_id = projections.apps7_oidc_configs.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.apps7_oidc_configs ON projections.apps7.id = projections.apps7_oidc_configs.app_id AND projections.apps7.instance_id = projections.apps7_oidc_configs.instance_id`) expectedProjectIDByAppQuery = regexp.QuoteMeta(`SELECT projections.apps7.project_id` + ` FROM projections.apps7` + ` LEFT JOIN projections.apps7_api_configs ON projections.apps7.id = projections.apps7_api_configs.app_id AND projections.apps7.instance_id = projections.apps7_api_configs.instance_id` + ` LEFT JOIN projections.apps7_oidc_configs ON projections.apps7.id = projections.apps7_oidc_configs.app_id AND projections.apps7.instance_id = projections.apps7_oidc_configs.instance_id` + - ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id`) expectedProjectByAppQuery = regexp.QuoteMeta(`SELECT projections.projects4.id,` + ` projections.projects4.creation_date,` + ` projections.projects4.change_date,` + @@ -140,8 +136,7 @@ var ( ` JOIN projections.apps7 ON projections.projects4.id = projections.apps7.project_id AND projections.projects4.instance_id = projections.apps7.instance_id` + ` LEFT JOIN projections.apps7_api_configs ON projections.apps7.id = projections.apps7_api_configs.app_id AND projections.apps7.instance_id = projections.apps7_api_configs.instance_id` + ` LEFT JOIN projections.apps7_oidc_configs ON projections.apps7.id = projections.apps7_oidc_configs.app_id AND projections.apps7.instance_id = projections.apps7_oidc_configs.instance_id` + - ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id`) appCols = database.TextArray[string]{ "id", @@ -1228,7 +1223,7 @@ func Test_AppsPrepare(t *testing.T) { if tt.name == "prepareAppsQuery oidc app" { _ = tt.name } - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -1246,8 +1241,8 @@ func Test_AppPrepare(t *testing.T) { }{ { name: "prepareAppQuery no result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueriesScanErr( @@ -1266,8 +1261,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery found", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQuery( @@ -1330,8 +1325,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery api app", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1400,8 +1395,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery oidc app", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1489,8 +1484,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery oidc app active only", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, true) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(true) }, want: want{ sqlExpectations: mockQueries( @@ -1578,8 +1573,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery saml app", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1651,8 +1646,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery oidc app IsDevMode inactive", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1740,8 +1735,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery oidc app AssertAccessTokenRole inactive", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1829,8 +1824,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery oidc app AssertIDTokenRole inactive", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1918,8 +1913,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery oidc app AssertIDTokenUserinfo inactive", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -2007,8 +2002,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueryErr( @@ -2027,7 +2022,7 @@ func Test_AppPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -2113,7 +2108,7 @@ func Test_AppIDsPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -2179,7 +2174,7 @@ func Test_ProjectIDByAppPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -2377,7 +2372,7 @@ func Test_ProjectByAppPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/auth_request.go b/internal/query/auth_request.go index 20ac0f5abd..eaf5e52491 100644 --- a/internal/query/auth_request.go +++ b/internal/query/auth_request.go @@ -5,13 +5,11 @@ import ( "database/sql" _ "embed" "errors" - "fmt" "time" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -44,10 +42,6 @@ func (a *AuthRequest) checkLoginClient(ctx context.Context, permissionCheck doma //go:embed auth_request_by_id.sql var authRequestByIDQuery string -func (q *Queries) authRequestByIDQuery(ctx context.Context) string { - return fmt.Sprintf(authRequestByIDQuery, q.client.Timetravel(call.Took(ctx))) -} - func (q *Queries) AuthRequestByID(ctx context.Context, shouldTriggerBulk bool, id string, checkLoginClient bool) (_ *AuthRequest, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -74,7 +68,7 @@ func (q *Queries) AuthRequestByID(ctx context.Context, shouldTriggerBulk bool, i &prompt, &locales, &dst.LoginHint, &dst.MaxAge, &dst.HintUserID, ) }, - q.authRequestByIDQuery(ctx), + authRequestByIDQuery, id, authz.GetInstance(ctx).InstanceID(), ) if errors.Is(err, sql.ErrNoRows) { diff --git a/internal/query/auth_request_by_id.sql b/internal/query/auth_request_by_id.sql index ffc18fccd6..f842719d0e 100644 --- a/internal/query/auth_request_by_id.sql +++ b/internal/query/auth_request_by_id.sql @@ -10,6 +10,6 @@ select login_hint, max_age, hint_user_id -from projections.auth_requests %s +from projections.auth_requests where id = $1 and instance_id = $2 limit 1; diff --git a/internal/query/auth_request_test.go b/internal/query/auth_request_test.go index 479282f9f7..152a032cd8 100644 --- a/internal/query/auth_request_test.go +++ b/internal/query/auth_request_test.go @@ -24,7 +24,6 @@ import ( func TestQueries_AuthRequestByID(t *testing.T) { expQuery := regexp.QuoteMeta(fmt.Sprintf( authRequestByIDQuery, - asOfSystemTime, )) cols := []string{ @@ -207,8 +206,7 @@ func TestQueries_AuthRequestByID(t *testing.T) { execMock(t, tt.expect, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, checkPermission: tt.permissionCheck, } diff --git a/internal/query/authn_key.go b/internal/query/authn_key.go index 6c05a03f6f..8075422e63 100644 --- a/internal/query/authn_key.go +++ b/internal/query/authn_key.go @@ -11,7 +11,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -129,7 +128,7 @@ func (q *Queries) SearchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQu ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareAuthNKeysQuery(ctx, q.client) + query, scan := prepareAuthNKeysQuery() query = queries.toQuery(query) eq := sq.Eq{ AuthNKeyColumnEnabled.identifier(): true, @@ -156,7 +155,7 @@ func (q *Queries) SearchAuthNKeysData(ctx context.Context, queries *AuthNKeySear ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareAuthNKeysDataQuery(ctx, q.client) + query, scan := prepareAuthNKeysDataQuery() query = queries.toQuery(query) eq := sq.Eq{ AuthNKeyColumnEnabled.identifier(): true, @@ -189,7 +188,7 @@ func (q *Queries) GetAuthNKeyByID(ctx context.Context, shouldTriggerBulk bool, i traceSpan.EndWithError(err) } - query, scan := prepareAuthNKeyQuery(ctx, q.client) + query, scan := prepareAuthNKeyQuery() for _, q := range queries { query = q.toQuery(query) } @@ -214,7 +213,7 @@ func (q *Queries) GetAuthNKeyPublicKeyByIDAndIdentifier(ctx context.Context, id ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareAuthNKeyPublicKeyQuery(ctx, q.client) + stmt, scan := prepareAuthNKeyPublicKeyQuery() eq := sq.And{ sq.Eq{ AuthNKeyColumnID.identifier(): id, @@ -288,7 +287,7 @@ func (q *Queries) GetAuthNKeyUser(ctx context.Context, keyID, userID string) (_ return dst, nil } -func prepareAuthNKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys, error)) { +func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys, error)) { return sq.Select( AuthNKeyColumnID.identifier(), AuthNKeyColumnCreationDate.identifier(), @@ -298,7 +297,7 @@ func prepareAuthNKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu AuthNKeyColumnExpiration.identifier(), AuthNKeyColumnType.identifier(), countColumn.identifier(), - ).From(authNKeyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(authNKeyTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*AuthNKeys, error) { authNKeys := make([]*AuthNKey, 0) @@ -334,7 +333,7 @@ func prepareAuthNKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } } -func prepareAuthNKeyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*AuthNKey, error)) { +func prepareAuthNKeyQuery() (sq.SelectBuilder, func(row *sql.Row) (*AuthNKey, error)) { return sq.Select( AuthNKeyColumnID.identifier(), AuthNKeyColumnCreationDate.identifier(), @@ -343,7 +342,7 @@ func prepareAuthNKeyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui AuthNKeyColumnSequence.identifier(), AuthNKeyColumnExpiration.identifier(), AuthNKeyColumnType.identifier(), - ).From(authNKeyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(authNKeyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*AuthNKey, error) { authNKey := new(AuthNKey) @@ -366,10 +365,10 @@ func prepareAuthNKeyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui } } -func prepareAuthNKeyPublicKeyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) ([]byte, error)) { +func prepareAuthNKeyPublicKeyQuery() (sq.SelectBuilder, func(row *sql.Row) ([]byte, error)) { return sq.Select( AuthNKeyColumnPublicKey.identifier(), - ).From(authNKeyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(authNKeyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) ([]byte, error) { var publicKey []byte @@ -386,7 +385,7 @@ func prepareAuthNKeyPublicKeyQuery(ctx context.Context, db prepareDatabase) (sq. } } -func prepareAuthNKeysDataQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeysData, error)) { +func prepareAuthNKeysDataQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeysData, error)) { return sq.Select( AuthNKeyColumnID.identifier(), AuthNKeyColumnCreationDate.identifier(), @@ -398,7 +397,7 @@ func prepareAuthNKeysDataQuery(ctx context.Context, db prepareDatabase) (sq.Sele AuthNKeyColumnIdentifier.identifier(), AuthNKeyColumnPublicKey.identifier(), countColumn.identifier(), - ).From(authNKeyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(authNKeyTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*AuthNKeysData, error) { authNKeys := make([]*AuthNKeyData, 0) diff --git a/internal/query/authn_key_test.go b/internal/query/authn_key_test.go index 19005893f8..c7441f8dae 100644 --- a/internal/query/authn_key_test.go +++ b/internal/query/authn_key_test.go @@ -26,8 +26,7 @@ var ( ` projections.authn_keys2.expiration,` + ` projections.authn_keys2.type,` + ` COUNT(*) OVER ()` + - ` FROM projections.authn_keys2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.authn_keys2` prepareAuthNKeysCols = []string{ "id", "creation_date", @@ -49,8 +48,7 @@ var ( ` projections.authn_keys2.identifier,` + ` projections.authn_keys2.public_key,` + ` COUNT(*) OVER ()` + - ` FROM projections.authn_keys2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.authn_keys2` prepareAuthNKeysDataCols = []string{ "id", "creation_date", @@ -71,8 +69,7 @@ var ( ` projections.authn_keys2.sequence,` + ` projections.authn_keys2.expiration,` + ` projections.authn_keys2.type` + - ` FROM projections.authn_keys2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.authn_keys2` prepareAuthNKeyCols = []string{ "id", "creation_date", @@ -84,8 +81,7 @@ var ( } prepareAuthNKeyPublicKeyStmt = `SELECT projections.authn_keys2.public_key` + - ` FROM projections.authn_keys2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.authn_keys2` prepareAuthNKeyPublicKeyCols = []string{ "public_key", } @@ -471,7 +467,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -525,8 +521,7 @@ low2kyJov38V4Uk2I8kuXpLcnrpw5Tio2ooiUE27b0vHZqBKOei9Uo88qCrn3EKx execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "userID") diff --git a/internal/query/certificate.go b/internal/query/certificate.go index e4d53213cf..ebe4b249f4 100644 --- a/internal/query/certificate.go +++ b/internal/query/certificate.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -69,7 +68,7 @@ func (q *Queries) ActiveCertificates(ctx context.Context, t time.Time, usage cry ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareCertificateQuery(ctx, q.client) + query, scan := prepareCertificateQuery() if t.IsZero() { t = time.Now() } @@ -102,7 +101,7 @@ func (q *Queries) ActiveCertificates(ctx context.Context, t time.Time, usage cry return certs, nil } -func prepareCertificateQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Certificates, error)) { +func prepareCertificateQuery() (sq.SelectBuilder, func(*sql.Rows) (*Certificates, error)) { return sq.Select( KeyColID.identifier(), KeyColCreationDate.identifier(), @@ -117,7 +116,7 @@ func prepareCertificateQuery(ctx context.Context, db prepareDatabase) (sq.Select countColumn.identifier(), ).From(keyTable.identifier()). LeftJoin(join(CertificateColID, KeyColID)). - LeftJoin(join(KeyPrivateColID, KeyColID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(KeyPrivateColID, KeyColID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Certificates, error) { certificates := make([]Certificate, 0) diff --git a/internal/query/certificate_test.go b/internal/query/certificate_test.go index 01e563de11..eae011bb69 100644 --- a/internal/query/certificate_test.go +++ b/internal/query/certificate_test.go @@ -26,8 +26,7 @@ var ( ` COUNT(*) OVER ()` + ` FROM projections.keys4` + ` LEFT JOIN projections.keys4_certificate ON projections.keys4.id = projections.keys4_certificate.id AND projections.keys4.instance_id = projections.keys4_certificate.instance_id` + - ` LEFT JOIN projections.keys4_private ON projections.keys4.id = projections.keys4_private.id AND projections.keys4.instance_id = projections.keys4_private.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.keys4_private ON projections.keys4.id = projections.keys4_private.id AND projections.keys4.instance_id = projections.keys4_private.instance_id` prepareCertificateCols = []string{ "id", "creation_date", @@ -142,7 +141,7 @@ func Test_CertificatePrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/current_state.go b/internal/query/current_state.go index 29497e6eec..6fae52713f 100644 --- a/internal/query/current_state.go +++ b/internal/query/current_state.go @@ -12,7 +12,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -68,7 +67,7 @@ func (q *Queries) SearchCurrentStates(ctx context.Context, queries *CurrentState ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareCurrentStateQuery(ctx, q.client) + query, scan := prepareCurrentStateQuery() stmt, args, err := queries.toQuery(query).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-MmFef", "Errors.Query.InvalidRequest") @@ -210,12 +209,12 @@ func reset(ctx context.Context, tx *sql.Tx, tables []string, projectionName stri return nil } -func prepareLatestState(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*State, error)) { +func prepareLatestState() (sq.SelectBuilder, func(*sql.Row) (*State, error)) { return sq.Select( CurrentStateColEventDate.identifier(), CurrentStateColPosition.identifier(), CurrentStateColLastUpdated.identifier()). - From(currentStateTable.identifier() + db.Timetravel(call.Took(ctx))). + From(currentStateTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*State, error) { var ( @@ -239,7 +238,7 @@ func prepareLatestState(ctx context.Context, db prepareDatabase) (sq.SelectBuild } } -func prepareCurrentStateQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*CurrentStates, error)) { +func prepareCurrentStateQuery() (sq.SelectBuilder, func(*sql.Rows) (*CurrentStates, error)) { return sq.Select( CurrentStateColLastUpdated.identifier(), CurrentStateColEventDate.identifier(), @@ -249,7 +248,7 @@ func prepareCurrentStateQuery(ctx context.Context, db prepareDatabase) (sq.Selec CurrentStateColAggregateID.identifier(), CurrentStateColSequence.identifier(), countColumn.identifier()). - From(currentStateTable.identifier() + db.Timetravel(call.Took(ctx))). + From(currentStateTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*CurrentStates, error) { states := make([]*CurrentState, 0) diff --git a/internal/query/current_state_test.go b/internal/query/current_state_test.go index c76dae710e..c0895dc439 100644 --- a/internal/query/current_state_test.go +++ b/internal/query/current_state_test.go @@ -19,8 +19,7 @@ var ( ` projections.current_states.aggregate_id,` + ` projections.current_states.sequence,` + ` COUNT(*) OVER ()` + - ` FROM projections.current_states` + - " AS OF SYSTEM TIME '-1 ms' " + ` FROM projections.current_states` currentSequenceCols = []string{ "last_updated", @@ -175,7 +174,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/custom_text.go b/internal/query/custom_text.go index e92c910b69..0bc909d614 100644 --- a/internal/query/custom_text.go +++ b/internal/query/custom_text.go @@ -13,7 +13,6 @@ import ( "sigs.k8s.io/yaml" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/i18n" @@ -90,7 +89,7 @@ func (q *Queries) CustomTextList(ctx context.Context, aggregateID, template, lan ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareCustomTextsQuery(ctx, q.client) + stmt, scan := prepareCustomTextsQuery() eq := sq.Eq{ CustomTextColAggregateID.identifier(): aggregateID, CustomTextColTemplate.identifier(): template, @@ -121,7 +120,7 @@ func (q *Queries) CustomTextListByTemplate(ctx context.Context, aggregateID, tem ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareCustomTextsQuery(ctx, q.client) + stmt, scan := prepareCustomTextsQuery() eq := sq.Eq{ CustomTextColAggregateID.identifier(): aggregateID, CustomTextColTemplate.identifier(): template, @@ -230,7 +229,7 @@ func (q *Queries) readLoginTranslationFile(ctx context.Context, lang string) ([] return contents, nil } -func prepareCustomTextsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*CustomTexts, error)) { +func prepareCustomTextsQuery() (sq.SelectBuilder, func(*sql.Rows) (*CustomTexts, error)) { return sq.Select( CustomTextColAggregateID.identifier(), CustomTextColSequence.identifier(), @@ -241,7 +240,7 @@ func prepareCustomTextsQuery(ctx context.Context, db prepareDatabase) (sq.Select CustomTextColKey.identifier(), CustomTextColText.identifier(), countColumn.identifier()). - From(customTextTable.identifier() + db.Timetravel(call.Took(ctx))). + From(customTextTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*CustomTexts, error) { customTexts := make([]*CustomText, 0) diff --git a/internal/query/custom_text_test.go b/internal/query/custom_text_test.go index 0453f71a2a..c31072793b 100644 --- a/internal/query/custom_text_test.go +++ b/internal/query/custom_text_test.go @@ -23,8 +23,7 @@ var ( ` projections.custom_texts2.key,` + ` projections.custom_texts2.text,` + ` COUNT(*) OVER ()` + - ` FROM projections.custom_texts2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.custom_texts2` prepareCustomTextsCols = []string{ "aggregate_id", "sequence", @@ -185,7 +184,7 @@ func Test_CustomTextPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/device_auth.go b/internal/query/device_auth.go index e42b5a114e..d2f86a44af 100644 --- a/internal/query/device_auth.go +++ b/internal/query/device_auth.go @@ -63,7 +63,7 @@ func (q *Queries) DeviceAuthRequestByUserCode(ctx context.Context, userCode stri ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareDeviceAuthQuery(ctx, q.client) + stmt, scan := prepareDeviceAuthQuery() eq := sq.Eq{ DeviceAuthRequestColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), DeviceAuthRequestColumnUserCode.identifier(): userCode, @@ -90,7 +90,7 @@ var deviceAuthSelectColumns = []string{ ProjectColumnName.identifier(), } -func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*domain.AuthRequestDevice, error)) { +func prepareDeviceAuthQuery() (sq.SelectBuilder, func(*sql.Row) (*domain.AuthRequestDevice, error)) { return sq.Select(deviceAuthSelectColumns...). From(deviceAuthRequestTable.identifier()). LeftJoin(join(AppOIDCConfigColumnClientID, DeviceAuthRequestColumnClientID)). diff --git a/internal/query/device_auth_test.go b/internal/query/device_auth_test.go index 6f0f82b3be..52ac50abb7 100644 --- a/internal/query/device_auth_test.go +++ b/internal/query/device_auth_test.go @@ -138,7 +138,7 @@ func Test_prepareDeviceAuthQuery(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, prepareDeviceAuthQuery, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, prepareDeviceAuthQuery, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/domain_policy.go b/internal/query/domain_policy.go index d971723bcf..3eba664e75 100644 --- a/internal/query/domain_policy.go +++ b/internal/query/domain_policy.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -118,7 +117,7 @@ func (q *Queries) DomainPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, } } - stmt, scan := prepareDomainPolicyQuery(ctx, q.client) + stmt, scan := prepareDomainPolicyQuery() query, args, err := stmt.Where(eq).OrderBy(DomainPolicyColIsDefault.identifier()). Limit(1).ToSql() if err != nil { @@ -136,7 +135,7 @@ func (q *Queries) DefaultDomainPolicy(ctx context.Context) (policy *DomainPolicy ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareDomainPolicyQuery(ctx, q.client) + stmt, scan := prepareDomainPolicyQuery() query, args, err := stmt.Where(sq.Eq{ DomainPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID(), DomainPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -154,7 +153,7 @@ func (q *Queries) DefaultDomainPolicy(ctx context.Context) (policy *DomainPolicy return policy, err } -func prepareDomainPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*DomainPolicy, error)) { +func prepareDomainPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*DomainPolicy, error)) { return sq.Select( DomainPolicyColID.identifier(), DomainPolicyColSequence.identifier(), @@ -167,7 +166,7 @@ func prepareDomainPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Selec DomainPolicyColIsDefault.identifier(), DomainPolicyColState.identifier(), ). - From(domainPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + From(domainPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*DomainPolicy, error) { policy := new(DomainPolicy) diff --git a/internal/query/domain_policy_test.go b/internal/query/domain_policy_test.go index 70d3ddc391..0ff2567979 100644 --- a/internal/query/domain_policy_test.go +++ b/internal/query/domain_policy_test.go @@ -23,8 +23,7 @@ var ( ` projections.domain_policies2.smtp_sender_address_matches_instance_domain,` + ` projections.domain_policies2.is_default,` + ` projections.domain_policies2.state` + - ` FROM projections.domain_policies2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.domain_policies2` prepareDomainPolicyCols = []string{ "id", "sequence", @@ -122,7 +121,7 @@ func Test_DomainPolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/execution.go b/internal/query/execution.go index b98c680f57..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" @@ -101,8 +103,8 @@ func (q *Queries) SearchExecutions(ctx context.Context, queries *ExecutionSearch eq := sq.Eq{ ExecutionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } - query, scan := prepareExecutionsQuery(ctx, q.client) - return genericRowsQueryWithState[*Executions](ctx, q.client, executionTable, combineToWhereStmt(query, queries.toQuery, eq), scan) + query, scan := prepareExecutionsQuery() + return genericRowsQueryWithState(ctx, q.client, executionTable, combineToWhereStmt(query, queries.toQuery, eq), scan) } func (q *Queries) GetExecutionByID(ctx context.Context, id string) (execution *Execution, err error) { @@ -110,8 +112,8 @@ func (q *Queries) GetExecutionByID(ctx context.Context, id string) (execution *E ExecutionColumnID.identifier(): id, ExecutionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } - query, scan := prepareExecutionQuery(ctx, q.client) - return genericRowQuery[*Execution](ctx, q.client, query.Where(eq), scan) + query, scan := prepareExecutionQuery() + return genericRowQuery(ctx, q.client, query.Where(eq), scan) } func NewExecutionInIDsSearchQuery(values []string) (SearchQuery, error) { @@ -219,7 +221,7 @@ func (q *Queries) TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string return execution, err } -func prepareExecutionQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) { +func prepareExecutionQuery() (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) { return sq.Select( ExecutionColumnInstanceID.identifier(), ExecutionColumnID.identifier(), @@ -235,7 +237,7 @@ func prepareExecutionQuery(context.Context, prepareDatabase) (sq.SelectBuilder, scanExecution } -func prepareExecutionsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) { +func prepareExecutionsQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) { return sq.Select( ExecutionColumnInstanceID.identifier(), ExecutionColumnID.identifier(), @@ -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 ee6bdc4d96..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, @@ -263,7 +322,7 @@ func Test_ExecutionPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/failed_events.go b/internal/query/failed_events.go index 7d2e875cee..c5ad1ae1d9 100644 --- a/internal/query/failed_events.go +++ b/internal/query/failed_events.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -83,7 +82,7 @@ type FailedEventSearchQueries struct { } func (q *Queries) SearchFailedEvents(ctx context.Context, queries *FailedEventSearchQueries) (failedEvents *FailedEvents, err error) { - query, scan := prepareFailedEventsQuery(ctx, q.client) + query, scan := prepareFailedEventsQuery() stmt, args, err := queries.toQuery(query).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-n8rjJ", "Errors.Query.InvalidRequest") @@ -139,7 +138,7 @@ func (q *FailedEventSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuil return query } -func prepareFailedEventsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*FailedEvents, error)) { +func prepareFailedEventsQuery() (sq.SelectBuilder, func(*sql.Rows) (*FailedEvents, error)) { return sq.Select( FailedEventsColumnProjectionName.identifier(), FailedEventsColumnFailedSequence.identifier(), @@ -149,7 +148,7 @@ func prepareFailedEventsQuery(ctx context.Context, db prepareDatabase) (sq.Selec FailedEventsColumnLastFailed.identifier(), FailedEventsColumnError.identifier(), countColumn.identifier()). - From(failedEventsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(failedEventsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*FailedEvents, error) { failedEvents := make([]*FailedEvent, 0) diff --git a/internal/query/failed_events_test.go b/internal/query/failed_events_test.go index 7e575b5891..25c15e9b8f 100644 --- a/internal/query/failed_events_test.go +++ b/internal/query/failed_events_test.go @@ -19,8 +19,7 @@ var ( ` projections.failed_events2.last_failed,` + ` projections.failed_events2.error,` + ` COUNT(*) OVER ()` + - ` FROM projections.failed_events2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.failed_events2` prepareFailedEventsCols = []string{ "projection_name", @@ -168,7 +167,7 @@ func Test_FailedEventsPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/generic.go b/internal/query/generic.go index 70fc2884a2..ea2257c013 100644 --- a/internal/query/generic.go +++ b/internal/query/generic.go @@ -44,7 +44,7 @@ func genericRowsQueryWithState[R Stateful]( scan func(rows *sql.Rows) (R, error), ) (resp R, err error) { var rnil R - resp, err = genericRowsQuery[R](ctx, client, query, scan) + resp, err = genericRowsQuery(ctx, client, query, scan) if err != nil { return rnil, err } @@ -60,7 +60,7 @@ func latestState(ctx context.Context, client *database.DB, projections ...table) ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLatestState(ctx, client) + query, scan := prepareLatestState() or := make(sq.Or, len(projections)) for i, projection := range projections { or[i] = sq.Eq{CurrentStateColProjectionName.identifier(): projection.name} diff --git a/internal/query/iam_member.go b/internal/query/iam_member.go index 87b906aa51..139208c7b8 100644 --- a/internal/query/iam_member.go +++ b/internal/query/iam_member.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -71,7 +70,7 @@ func (q *Queries) IAMMembers(ctx context.Context, queries *IAMMembersQuery) (mem ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareInstanceMembersQuery(ctx, q.client) + query, scan := prepareInstanceMembersQuery() eq := sq.Eq{InstanceMemberInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -94,7 +93,7 @@ func (q *Queries) IAMMembers(ctx context.Context, queries *IAMMembersQuery) (mem return members, err } -func prepareInstanceMembersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { +func prepareInstanceMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { return sq.Select( InstanceMemberCreationDate.identifier(), InstanceMemberChangeDate.identifier(), @@ -116,7 +115,7 @@ func prepareInstanceMembersQuery(ctx context.Context, db prepareDatabase) (sq.Se LeftJoin(join(HumanUserIDCol, InstanceMemberUserID)). LeftJoin(join(MachineUserIDCol, InstanceMemberUserID)). LeftJoin(join(UserIDCol, InstanceMemberUserID)). - LeftJoin(join(LoginNameUserIDCol, InstanceMemberUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, InstanceMemberUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/iam_member_test.go b/internal/query/iam_member_test.go index 82cea360c8..5c10ebc5bc 100644 --- a/internal/query/iam_member_test.go +++ b/internal/query/iam_member_test.go @@ -39,7 +39,6 @@ var ( "ON members.user_id = projections.users14.id AND members.instance_id = projections.users14.instance_id " + "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id AND members.instance_id = projections.login_names3.instance_id " + - "AS OF SYSTEM TIME '-1 ms' " + "WHERE projections.login_names3.is_primary = $1") instanceMembersColumns = []string{ "creation_date", @@ -295,7 +294,7 @@ func Test_IAMMemberPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/idp.go b/internal/query/idp.go index 2687397330..4f0d77c62b 100644 --- a/internal/query/idp.go +++ b/internal/query/idp.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" @@ -215,7 +214,7 @@ func (q *Queries) IDPByIDAndResourceOwner(ctx context.Context, shouldTriggerBulk sq.Eq{IDPResourceOwnerCol.identifier(): authz.GetInstance(ctx).InstanceID()}, }, } - stmt, scan := prepareIDPByIDQuery(ctx, q.client) + stmt, scan := prepareIDPByIDQuery() query, args, err := stmt.Where(where).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-0gocI", "Errors.Query.SQLStatement") @@ -233,7 +232,7 @@ func (q *Queries) IDPs(ctx context.Context, queries *IDPSearchQueries, withOwner ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareIDPsQuery(ctx, q.client) + query, scan := prepareIDPsQuery() eq := sq.Eq{ IDPInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -293,7 +292,7 @@ func (q *IDPSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { return query } -func prepareIDPByIDQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*IDP, error)) { +func prepareIDPByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDP, error)) { return sq.Select( IDPIDCol.identifier(), IDPResourceOwnerCol.identifier(), @@ -321,7 +320,7 @@ func prepareIDPByIDQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil JWTIDPColEndpoint.identifier(), ).From(idpTable.identifier()). LeftJoin(join(OIDCIDPColIDPID, IDPIDCol)). - LeftJoin(join(JWTIDPColIDPID, IDPIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(JWTIDPColIDPID, IDPIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*IDP, error) { idp := new(IDP) @@ -401,7 +400,7 @@ func prepareIDPByIDQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } } -func prepareIDPsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPs, error)) { +func prepareIDPsQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPs, error)) { return sq.Select( IDPIDCol.identifier(), IDPResourceOwnerCol.identifier(), @@ -430,7 +429,7 @@ func prepareIDPsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder countColumn.identifier(), ).From(idpTable.identifier()). LeftJoin(join(OIDCIDPColIDPID, IDPIDCol)). - LeftJoin(join(JWTIDPColIDPID, IDPIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(JWTIDPColIDPID, IDPIDCol)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPs, error) { idps := make([]*IDP, 0) diff --git a/internal/query/idp_login_policy_link.go b/internal/query/idp_login_policy_link.go index bdc2ef15b1..65f855bc51 100644 --- a/internal/query/idp_login_policy_link.go +++ b/internal/query/idp_login_policy_link.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -97,7 +96,7 @@ func (q *Queries) IDPLoginPolicyLinks(ctx context.Context, resourceOwner string, ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareIDPLoginPolicyLinksQuery(ctx, q.client, resourceOwner) + query, scan := prepareIDPLoginPolicyLinksQuery(ctx, resourceOwner) eq := sq.Eq{ IDPLoginPolicyLinkInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -122,7 +121,8 @@ func (q *Queries) IDPLoginPolicyLinks(ctx context.Context, resourceOwner string, return idps, err } -func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase, resourceOwner string) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { +//nolint:gocognit +func prepareIDPLoginPolicyLinksQuery(ctx context.Context, resourceOwner string) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { resourceOwnerQuery, resourceOwnerArgs, err := prepareIDPLoginPolicyLinksResourceOwnerQuery(ctx, resourceOwner) if err != nil { return sq.SelectBuilder{}, nil @@ -142,8 +142,7 @@ func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase, re LeftJoin(join(IDPTemplateIDCol, IDPLoginPolicyLinkIDPIDCol)). RightJoin("("+resourceOwnerQuery+") AS "+idpLoginPolicyOwnerTable.alias+" ON "+ idpLoginPolicyOwnerIDCol.identifier()+" = "+IDPLoginPolicyLinkResourceOwnerCol.identifier()+" AND "+ - idpLoginPolicyOwnerInstanceIDCol.identifier()+" = "+IDPLoginPolicyLinkInstanceIDCol.identifier()+ - " "+db.Timetravel(call.Took(ctx)), + idpLoginPolicyOwnerInstanceIDCol.identifier()+" = "+IDPLoginPolicyLinkInstanceIDCol.identifier(), resourceOwnerArgs...). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPLoginPolicyLinks, error) { diff --git a/internal/query/idp_login_policy_link_test.go b/internal/query/idp_login_policy_link_test.go index 245eb22ccc..9f66e118ea 100644 --- a/internal/query/idp_login_policy_link_test.go +++ b/internal/query/idp_login_policy_link_test.go @@ -6,6 +6,7 @@ import ( "database/sql/driver" "errors" "fmt" + "reflect" "regexp" "testing" @@ -29,8 +30,7 @@ var ( ` LEFT JOIN projections.idp_templates6 ON projections.idp_login_policy_links5.idp_id = projections.idp_templates6.id AND projections.idp_login_policy_links5.instance_id = projections.idp_templates6.instance_id` + ` RIGHT JOIN (SELECT login_policy_owner.aggregate_id, login_policy_owner.instance_id, login_policy_owner.owner_removed FROM projections.login_policies5 AS login_policy_owner` + ` WHERE (login_policy_owner.instance_id = $1 AND (login_policy_owner.aggregate_id = $2 OR login_policy_owner.aggregate_id = $3)) ORDER BY login_policy_owner.is_default LIMIT 1) AS login_policy_owner` + - ` ON login_policy_owner.aggregate_id = projections.idp_login_policy_links5.resource_owner AND login_policy_owner.instance_id = projections.idp_login_policy_links5.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` ON login_policy_owner.aggregate_id = projections.idp_login_policy_links5.resource_owner AND login_policy_owner.instance_id = projections.idp_login_policy_links5.instance_id`) loginPolicyIDPLinksCols = []string{ "idp_id", "name", @@ -52,14 +52,14 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { } tests := []struct { name string - prepare interface{} + prepare any want want - object interface{} + object any }{ { name: "prepareIDPsQuery found", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { - return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner") + prepare: func(ctx context.Context) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + return prepareIDPLoginPolicyLinksQuery(ctx, "resourceOwner") }, want: want{ sqlExpectations: mockQueries( @@ -101,8 +101,8 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { }, { name: "prepareIDPsQuery no idp", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { - return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner") + prepare: func(ctx context.Context) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + return prepareIDPLoginPolicyLinksQuery(ctx, "resourceOwner") }, want: want{ sqlExpectations: mockQueries( @@ -143,8 +143,8 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { }, { name: "prepareIDPsQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { - return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner") + prepare: func(ctx context.Context) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + return prepareIDPLoginPolicyLinksQuery(ctx, "resourceOwner") }, want: want{ sqlExpectations: mockQueryErr( @@ -163,7 +163,7 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, reflect.ValueOf(context.Background())) }) } } diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index a63cb6f485..f51e9a11a7 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" @@ -767,7 +766,7 @@ func (q *Queries) idpTemplateByID(ctx context.Context, shouldTriggerBulk bool, i if !withOwnerRemoved { eq[IDPTemplateOwnerRemovedCol.identifier()] = false } - query, scan := prepareIDPTemplateByIDQuery(ctx, q.client) + query, scan := prepareIDPTemplateByIDQuery() for _, q := range queries { query = q.toQuery(query) } @@ -788,7 +787,7 @@ func (q *Queries) IDPTemplates(ctx context.Context, queries *IDPTemplateSearchQu ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareIDPTemplatesQuery(ctx, q.client) + query, scan := prepareIDPTemplatesQuery() eq := sq.Eq{ IDPTemplateInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -864,7 +863,7 @@ func (q *IDPTemplateSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuil return query } -func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*IDPTemplate, error)) { +func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTemplate, error)) { return sq.Select( IDPTemplateIDCol.identifier(), IDPTemplateResourceOwnerCol.identifier(), @@ -993,7 +992,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)). LeftJoin(join(SAMLIDCol, IDPTemplateIDCol)). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)). - LeftJoin(join(AppleIDCol, IDPTemplateIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(AppleIDCol, IDPTemplateIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*IDPTemplate, error) { idpTemplate := new(IDPTemplate) @@ -1371,7 +1370,8 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se } } -func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplates, error)) { +//nolint:gocognit +func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplates, error)) { return sq.Select( IDPTemplateIDCol.identifier(), IDPTemplateResourceOwnerCol.identifier(), @@ -1502,7 +1502,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)). LeftJoin(join(SAMLIDCol, IDPTemplateIDCol)). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)). - LeftJoin(join(AppleIDCol, IDPTemplateIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(AppleIDCol, IDPTemplateIDCol)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPTemplates, error) { templates := make([]*IDPTemplate, 0) diff --git a/internal/query/idp_template_test.go b/internal/query/idp_template_test.go index 07fb80d78a..702e5d0ced 100644 --- a/internal/query/idp_template_test.go +++ b/internal/query/idp_template_test.go @@ -143,8 +143,7 @@ var ( ` LEFT JOIN projections.idp_templates6_google ON projections.idp_templates6.id = projections.idp_templates6_google.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_google.instance_id` + ` LEFT JOIN projections.idp_templates6_saml ON projections.idp_templates6.id = projections.idp_templates6_saml.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_saml.instance_id` + ` LEFT JOIN projections.idp_templates6_ldap2 ON projections.idp_templates6.id = projections.idp_templates6_ldap2.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_ldap2.instance_id` + - ` LEFT JOIN projections.idp_templates6_apple ON projections.idp_templates6.id = projections.idp_templates6_apple.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_apple.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.idp_templates6_apple ON projections.idp_templates6.id = projections.idp_templates6_apple.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_apple.instance_id` idpTemplateCols = []string{ "id", "resource_owner", @@ -390,8 +389,7 @@ var ( ` LEFT JOIN projections.idp_templates6_google ON projections.idp_templates6.id = projections.idp_templates6_google.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_google.instance_id` + ` LEFT JOIN projections.idp_templates6_saml ON projections.idp_templates6.id = projections.idp_templates6_saml.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_saml.instance_id` + ` LEFT JOIN projections.idp_templates6_ldap2 ON projections.idp_templates6.id = projections.idp_templates6_ldap2.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_ldap2.instance_id` + - ` LEFT JOIN projections.idp_templates6_apple ON projections.idp_templates6.id = projections.idp_templates6_apple.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_apple.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.idp_templates6_apple ON projections.idp_templates6.id = projections.idp_templates6_apple.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_apple.instance_id` idpTemplatesCols = []string{ "id", "resource_owner", @@ -3485,7 +3483,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/idp_test.go b/internal/query/idp_test.go index 9474a0c751..a7f6fb95c1 100644 --- a/internal/query/idp_test.go +++ b/internal/query/idp_test.go @@ -733,7 +733,7 @@ func Test_IDPPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/idp_user_link.go b/internal/query/idp_user_link.go index 5caf6c6646..23305dfd6e 100644 --- a/internal/query/idp_user_link.go +++ b/internal/query/idp_user_link.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -129,7 +128,7 @@ func (q *Queries) idpUserLinks(ctx context.Context, queries *IDPUserLinksSearchQ ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareIDPUserLinksQuery(ctx, q.client) + query, scan := prepareIDPUserLinksQuery() eq := sq.Eq{IDPUserLinkInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} if !withOwnerRemoved { eq[IDPUserLinkOwnerRemovedCol.identifier()] = false @@ -166,7 +165,7 @@ func NewIDPUserLinksExternalIDSearchQuery(value string) (SearchQuery, error) { return NewTextQuery(IDPUserLinkExternalUserIDCol, value, TextEquals) } -func prepareIDPUserLinksQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPUserLinks, error)) { +func prepareIDPUserLinksQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPUserLinks, error)) { return sq.Select( IDPUserLinkIDPIDCol.identifier(), IDPUserLinkUserIDCol.identifier(), @@ -177,7 +176,7 @@ func prepareIDPUserLinksQuery(ctx context.Context, db prepareDatabase) (sq.Selec IDPUserLinkResourceOwnerCol.identifier(), countColumn.identifier()). From(idpUserLinkTable.identifier()). - LeftJoin(join(IDPTemplateIDCol, IDPUserLinkIDPIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(IDPTemplateIDCol, IDPUserLinkIDPIDCol)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPUserLinks, error) { idps := make([]*IDPUserLink, 0) diff --git a/internal/query/idp_user_link_test.go b/internal/query/idp_user_link_test.go index b8ba2d087a..eac9669110 100644 --- a/internal/query/idp_user_link_test.go +++ b/internal/query/idp_user_link_test.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "regexp" + "slices" "testing" "github.com/stretchr/testify/require" @@ -165,10 +166,8 @@ func TestUser_idpLinksCheckPermission(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { checkPermission := func(ctx context.Context, permission, orgID, resourceID string) (err error) { - for _, perm := range tt.permissions { - if resourceID == perm { - return nil - } + if slices.Contains(tt.permissions, resourceID) { + return nil } return errors.New("failed") } @@ -188,8 +187,7 @@ var ( ` projections.idp_user_links3.resource_owner,` + ` COUNT(*) OVER ()` + ` FROM projections.idp_user_links3` + - ` LEFT JOIN projections.idp_templates6 ON projections.idp_user_links3.idp_id = projections.idp_templates6.id AND projections.idp_user_links3.instance_id = projections.idp_templates6.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.idp_templates6 ON projections.idp_user_links3.idp_id = projections.idp_templates6.id AND projections.idp_user_links3.instance_id = projections.idp_templates6.instance_id`) idpUserLinksCols = []string{ "idp_id", "user_id", @@ -307,7 +305,7 @@ func Test_IDPUserLinkPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/instance.go b/internal/query/instance.go index d7d66b1607..1b3bb055cb 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -16,7 +16,6 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -151,7 +150,7 @@ func (q *Queries) SearchInstances(ctx context.Context, queries *InstanceSearchQu ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - filter, query, scan := prepareInstancesQuery(ctx, q.client) + filter, query, scan := prepareInstancesQuery() stmt, args, err := query(queries.toQuery(filter)).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-M9fow", "Errors.Query.SQLStatement") @@ -178,7 +177,7 @@ func (q *Queries) Instance(ctx context.Context, shouldTriggerBulk bool) (instanc traceSpan.EndWithError(err) } - stmt, scan := prepareInstanceDomainQuery(ctx, q.client) + stmt, scan := prepareInstanceDomainQuery() query, args, err := stmt.Where(sq.Eq{ InstanceColumnID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() @@ -261,7 +260,7 @@ func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag { return instance.DefaultLang } -func prepareInstancesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(sq.SelectBuilder) sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { +func prepareInstancesQuery() (sq.SelectBuilder, func(sq.SelectBuilder) sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { instanceFilterTable := instanceTable.setAlias(InstancesFilterTableAlias) instanceFilterIDColumn := InstanceColumnID.setTable(instanceFilterTable) instanceFilterCountColumn := InstancesFilterTableAlias + ".count" @@ -291,7 +290,7 @@ func prepareInstancesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu InstanceDomainSequenceCol.identifier(), ).FromSelect(builder, InstancesFilterTableAlias). LeftJoin(join(InstanceColumnID, instanceFilterIDColumn)). - LeftJoin(join(InstanceDomainInstanceIDCol, instanceFilterIDColumn) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(InstanceDomainInstanceIDCol, instanceFilterIDColumn)). PlaceholderFormat(sq.Dollar) }, func(rows *sql.Rows) (*Instances, error) { @@ -366,7 +365,7 @@ func prepareInstancesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } } -func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instance, error)) { +func prepareInstanceDomainQuery() (sq.SelectBuilder, func(*sql.Rows) (*Instance, error)) { return sq.Select( InstanceColumnID.identifier(), InstanceColumnCreationDate.identifier(), @@ -386,7 +385,7 @@ func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase) (sq.Sel InstanceDomainSequenceCol.identifier(), ). From(instanceTable.identifier()). - LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Instance, error) { instance := &Instance{ diff --git a/internal/query/instance_domain.go b/internal/query/instance_domain.go index 285bd12936..47b5fab27f 100644 --- a/internal/query/instance_domain.go +++ b/internal/query/instance_domain.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -62,7 +61,7 @@ func (q *Queries) SearchInstanceDomains(ctx context.Context, queries *InstanceDo ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareInstanceDomainsQuery(ctx, q.client) + query, scan := prepareInstanceDomainsQuery() stmt, args, err := queries.toQuery(query). Where(sq.Eq{ InstanceDomainInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -78,7 +77,7 @@ func (q *Queries) SearchInstanceDomainsGlobal(ctx context.Context, queries *Inst ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareInstanceDomainsQuery(ctx, q.client) + query, scan := prepareInstanceDomainsQuery() stmt, args, err := queries.toQuery(query).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-IHhLR", "Errors.Query.SQLStatement") @@ -99,7 +98,7 @@ func (q *Queries) queryInstanceDomains(ctx context.Context, stmt string, scan fu return domains, err } -func prepareInstanceDomainsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*InstanceDomains, error)) { +func prepareInstanceDomainsQuery() (sq.SelectBuilder, func(*sql.Rows) (*InstanceDomains, error)) { return sq.Select( InstanceDomainCreationDateCol.identifier(), InstanceDomainChangeDateCol.identifier(), @@ -109,7 +108,7 @@ func prepareInstanceDomainsQuery(ctx context.Context, db prepareDatabase) (sq.Se InstanceDomainIsGeneratedCol.identifier(), InstanceDomainIsPrimaryCol.identifier(), countColumn.identifier(), - ).From(instanceDomainsTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(instanceDomainsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*InstanceDomains, error) { domains := make([]*InstanceDomain, 0) diff --git a/internal/query/instance_domain_test.go b/internal/query/instance_domain_test.go index 4f72c0def4..fd147bf4b7 100644 --- a/internal/query/instance_domain_test.go +++ b/internal/query/instance_domain_test.go @@ -18,8 +18,7 @@ var ( ` projections.instance_domains.is_generated,` + ` projections.instance_domains.is_primary,` + ` COUNT(*) OVER ()` + - ` FROM projections.instance_domains` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.instance_domains` prepareInstanceDomainsCols = []string{ "creation_date", "change_date", @@ -167,7 +166,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/instance_features.go b/internal/query/instance_features.go index 911edaa606..4ec40dc9d5 100644 --- a/internal/query/instance_features.go +++ b/internal/query/instance_features.go @@ -14,7 +14,6 @@ type InstanceFeatures struct { LegacyIntrospection FeatureSource[bool] UserSchema FeatureSource[bool] TokenExchange FeatureSource[bool] - Actions FeatureSource[bool] ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] WebKey FeatureSource[bool] DebugOIDCParentError FeatureSource[bool] diff --git a/internal/query/instance_features_model.go b/internal/query/instance_features_model.go index 11deb30f34..6a0abbb58c 100644 --- a/internal/query/instance_features_model.go +++ b/internal/query/instance_features_model.go @@ -67,7 +67,6 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceLegacyIntrospectionEventType, feature_v2.InstanceUserSchemaEventType, feature_v2.InstanceTokenExchangeEventType, - feature_v2.InstanceActionsEventType, feature_v2.InstanceImprovedPerformanceEventType, feature_v2.InstanceWebKeyEventType, feature_v2.InstanceDebugOIDCParentErrorEventType, @@ -98,7 +97,6 @@ func (m *InstanceFeaturesReadModel) populateFromSystem() bool { m.instance.LegacyIntrospection = m.system.LegacyIntrospection m.instance.UserSchema = m.system.UserSchema m.instance.TokenExchange = m.system.TokenExchange - m.instance.Actions = m.system.Actions m.instance.ImprovedPerformance = m.system.ImprovedPerformance m.instance.OIDCSingleV1SessionTermination = m.system.OIDCSingleV1SessionTermination m.instance.DisableUserTokenEvent = m.system.DisableUserTokenEvent @@ -113,7 +111,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_ return err } switch key { - case feature.KeyUnspecified: + case feature.KeyUnspecified, + feature.KeyActionsDeprecated: return nil case feature.KeyLoginDefaultOrg: features.LoginDefaultOrg.set(level, event.Value) @@ -125,8 +124,6 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_ features.UserSchema.set(level, event.Value) case feature.KeyTokenExchange: features.TokenExchange.set(level, event.Value) - case feature.KeyActions: - features.Actions.set(level, event.Value) case feature.KeyImprovedPerformance: features.ImprovedPerformance.set(level, event.Value) case feature.KeyWebKey: diff --git a/internal/query/instance_features_test.go b/internal/query/instance_features_test.go index e182f4002f..d80a3b05fc 100644 --- a/internal/query/instance_features_test.go +++ b/internal/query/instance_features_test.go @@ -84,31 +84,27 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { { name: "all features set", eventstore: expectEventstore( - expectFilter(eventFromEventPusher(feature_v2.NewSetEvent[bool]( + expectFilter(eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, true, ))), expectFilter( - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceLegacyIntrospectionEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceActionsEventType, false, - )), ), ), args: args{true}, @@ -132,45 +128,37 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelInstance, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: false, - }, }, }, { name: "all features set, reset, set some feature, cascaded", eventstore: expectEventstore( - expectFilter(eventFromEventPusher(feature_v2.NewSetEvent[bool]( + expectFilter(eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, true, ))), expectFilter( - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceLegacyIntrospectionEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceActionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( ctx, aggregate, feature_v2.InstanceResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, )), @@ -197,41 +185,33 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, { name: "all features set, reset, set some feature, not cascaded", eventstore: expectEventstore( expectFilter( - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceLegacyIntrospectionEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceActionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( ctx, aggregate, feature_v2.InstanceResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, )), @@ -258,10 +238,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, } diff --git a/internal/query/instance_test.go b/internal/query/instance_test.go index 8d9c7e1597..55b1c8314b 100644 --- a/internal/query/instance_test.go +++ b/internal/query/instance_test.go @@ -1,7 +1,6 @@ package query import ( - "context" "database/sql" "database/sql/driver" "errors" @@ -34,8 +33,7 @@ var ( ` FROM (SELECT DISTINCT projections.instances.id, COUNT(*) OVER () FROM projections.instances` + ` LEFT JOIN projections.instance_domains ON projections.instances.id = projections.instance_domains.instance_id) AS f` + ` LEFT JOIN projections.instances ON f.id = projections.instances.id` + - ` LEFT JOIN projections.instance_domains ON f.id = projections.instance_domains.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.instance_domains ON f.id = projections.instance_domains.instance_id` instancesCols = []string{ "count", "id", @@ -64,15 +62,15 @@ func Test_InstancePrepares(t *testing.T) { } tests := []struct { name string - prepare interface{} + prepare any additionalArgs []reflect.Value want want - object interface{} + object any }{ { name: "prepareInstancesQuery no result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery(ctx, db) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { + filter, query, scan := prepareInstancesQuery() return query(filter), scan }, want: want{ @@ -86,8 +84,8 @@ func Test_InstancePrepares(t *testing.T) { }, { name: "prepareInstancesQuery one result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery(ctx, db) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { + filter, query, scan := prepareInstancesQuery() return query(filter), scan }, want: want{ @@ -150,8 +148,8 @@ func Test_InstancePrepares(t *testing.T) { }, { name: "prepareInstancesQuery multiple results", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery(ctx, db) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { + filter, query, scan := prepareInstancesQuery() return query(filter), scan }, want: want{ @@ -283,8 +281,8 @@ func Test_InstancePrepares(t *testing.T) { }, { name: "prepareInstancesQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery(ctx, db) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { + filter, query, scan := prepareInstancesQuery() return query(filter), scan }, want: want{ @@ -304,7 +302,7 @@ func Test_InstancePrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, append(defaultPrepareArgs, tt.additionalArgs...)...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, tt.additionalArgs...) }) } } diff --git a/internal/query/instance_trusted_domain.go b/internal/query/instance_trusted_domain.go index 2847c3969a..8c3fd99987 100644 --- a/internal/query/instance_trusted_domain.go +++ b/internal/query/instance_trusted_domain.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -48,7 +47,7 @@ func (q *Queries) SearchInstanceTrustedDomains(ctx context.Context, queries *Ins ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareInstanceTrustedDomainsQuery(ctx, q.client) + query, scan := prepareInstanceTrustedDomainsQuery() stmt, args, err := queries.toQuery(query). Where(sq.Eq{ InstanceTrustedDomainInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -72,7 +71,7 @@ func (q *Queries) queryInstanceTrustedDomains(ctx context.Context, stmt string, return domains, err } -func prepareInstanceTrustedDomainsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*InstanceTrustedDomains, error)) { +func prepareInstanceTrustedDomainsQuery() (sq.SelectBuilder, func(*sql.Rows) (*InstanceTrustedDomains, error)) { return sq.Select( InstanceTrustedDomainCreationDateCol.identifier(), InstanceTrustedDomainChangeDateCol.identifier(), @@ -80,7 +79,7 @@ func prepareInstanceTrustedDomainsQuery(ctx context.Context, db prepareDatabase) InstanceTrustedDomainDomainCol.identifier(), InstanceTrustedDomainInstanceIDCol.identifier(), countColumn.identifier(), - ).From(instanceTrustedDomainsTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(instanceTrustedDomainsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*InstanceTrustedDomains, error) { domains := make([]*InstanceTrustedDomain, 0) diff --git a/internal/query/instance_trusted_domain_test.go b/internal/query/instance_trusted_domain_test.go index 6e3eea027e..518d2edb6b 100644 --- a/internal/query/instance_trusted_domain_test.go +++ b/internal/query/instance_trusted_domain_test.go @@ -16,8 +16,7 @@ var ( ` projections.instance_trusted_domains.domain,` + ` projections.instance_trusted_domains.instance_id,` + ` COUNT(*) OVER ()` + - ` FROM projections.instance_trusted_domains` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.instance_trusted_domains` prepareInstanceTrustedDomainsCols = []string{ "creation_date", "change_date", @@ -151,7 +150,7 @@ func Test_InstanceTrustedDomainPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/introspection_test.go b/internal/query/introspection_test.go index 4346842bf9..92c571ebf9 100644 --- a/internal/query/introspection_test.go +++ b/internal/query/introspection_test.go @@ -91,8 +91,7 @@ func TestQueries_ActiveIntrospectionClientByID(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "userID") diff --git a/internal/query/key.go b/internal/query/key.go index d7475e424b..4831d88654 100644 --- a/internal/query/key.go +++ b/internal/query/key.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query/projection" @@ -182,7 +181,7 @@ func (q *Queries) ActivePublicKeys(ctx context.Context, t time.Time) (keys *Publ ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := preparePublicKeysQuery(ctx, q.client) + query, scan := preparePublicKeysQuery() if t.IsZero() { t = time.Now() } @@ -214,7 +213,7 @@ func (q *Queries) ActivePrivateSigningKey(ctx context.Context, t time.Time) (key ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := preparePrivateKeysQuery(ctx, q.client) + stmt, scan := preparePrivateKeysQuery() if t.IsZero() { t = time.Now() } @@ -244,7 +243,7 @@ func (q *Queries) ActivePrivateSigningKey(ctx context.Context, t time.Time) (key return keys, nil } -func preparePublicKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*PublicKeys, error)) { +func preparePublicKeysQuery() (sq.SelectBuilder, func(*sql.Rows) (*PublicKeys, error)) { return sq.Select( KeyColID.identifier(), KeyColCreationDate.identifier(), @@ -257,7 +256,7 @@ func preparePublicKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectB KeyPublicColKey.identifier(), countColumn.identifier(), ).From(keyTable.identifier()). - LeftJoin(join(KeyPublicColID, KeyColID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(KeyPublicColID, KeyColID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*PublicKeys, error) { keys := make([]PublicKey, 0) @@ -300,7 +299,7 @@ func preparePublicKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectB } } -func preparePrivateKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*PrivateKeys, error)) { +func preparePrivateKeysQuery() (sq.SelectBuilder, func(*sql.Rows) (*PrivateKeys, error)) { return sq.Select( KeyColID.identifier(), KeyColCreationDate.identifier(), @@ -313,7 +312,7 @@ func preparePrivateKeysQuery(ctx context.Context, db prepareDatabase) (sq.Select KeyPrivateColKey.identifier(), countColumn.identifier(), ).From(keyTable.identifier()). - LeftJoin(join(KeyPrivateColID, KeyColID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(KeyPrivateColID, KeyColID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*PrivateKeys, error) { keys := make([]PrivateKey, 0) diff --git a/internal/query/key_test.go b/internal/query/key_test.go index a977bfb58e..7bc029fd7f 100644 --- a/internal/query/key_test.go +++ b/internal/query/key_test.go @@ -36,8 +36,7 @@ var ( ` projections.keys4_public.key,` + ` COUNT(*) OVER ()` + ` FROM projections.keys4` + - ` LEFT JOIN projections.keys4_public ON projections.keys4.id = projections.keys4_public.id AND projections.keys4.instance_id = projections.keys4_public.instance_id` + - ` AS OF SYSTEM TIME '-1 ms' ` + ` LEFT JOIN projections.keys4_public ON projections.keys4.id = projections.keys4_public.id AND projections.keys4.instance_id = projections.keys4_public.instance_id` preparePublicKeysCols = []string{ "id", "creation_date", @@ -62,8 +61,7 @@ var ( ` projections.keys4_private.key,` + ` COUNT(*) OVER ()` + ` FROM projections.keys4` + - ` LEFT JOIN projections.keys4_private ON projections.keys4.id = projections.keys4_private.id AND projections.keys4.instance_id = projections.keys4_private.instance_id` + - ` AS OF SYSTEM TIME '-1 ms' ` + ` LEFT JOIN projections.keys4_private ON projections.keys4.id = projections.keys4_private.id AND projections.keys4.instance_id = projections.keys4_private.instance_id` ) func Test_KeyPrepares(t *testing.T) { @@ -244,7 +242,7 @@ func Test_KeyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/label_policy.go b/internal/query/label_policy.go index 6dc7b00922..d3952a210a 100644 --- a/internal/query/label_policy.go +++ b/internal/query/label_policy.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -47,7 +46,7 @@ func (q *Queries) ActiveLabelPolicyByOrg(ctx context.Context, orgID string, with ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareLabelPolicyQuery(ctx, q.client) + stmt, scan := prepareLabelPolicyQuery() eq := sq.Eq{ LabelPolicyColState.identifier(): domain.LabelPolicyStateActive, LabelPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -80,7 +79,7 @@ func (q *Queries) PreviewLabelPolicyByOrg(ctx context.Context, orgID string) (po ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareLabelPolicyQuery(ctx, q.client) + stmt, scan := prepareLabelPolicyQuery() query, args, err := stmt.Where( sq.And{ sq.Or{ @@ -113,7 +112,7 @@ func (q *Queries) DefaultActiveLabelPolicy(ctx context.Context) (policy *LabelPo ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareLabelPolicyQuery(ctx, q.client) + stmt, scan := prepareLabelPolicyQuery() query, args, err := stmt.Where(sq.Eq{ LabelPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID(), LabelPolicyColState.identifier(): domain.LabelPolicyStateActive, @@ -136,7 +135,7 @@ func (q *Queries) DefaultPreviewLabelPolicy(ctx context.Context) (policy *LabelP ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareLabelPolicyQuery(ctx, q.client) + stmt, scan := prepareLabelPolicyQuery() query, args, err := stmt.Where(sq.Eq{ LabelPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID(), LabelPolicyColState.identifier(): domain.LabelPolicyStatePreview, @@ -240,7 +239,7 @@ var ( } ) -func prepareLabelPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*LabelPolicy, error)) { +func prepareLabelPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LabelPolicy, error)) { return sq.Select( LabelPolicyColCreationDate.identifier(), LabelPolicyColChangeDate.identifier(), @@ -270,7 +269,7 @@ func prepareLabelPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Select LabelPolicyColDarkLogoURL.identifier(), LabelPolicyColDarkIconURL.identifier(), ). - From(labelPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + From(labelPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*LabelPolicy, error) { policy := new(LabelPolicy) diff --git a/internal/query/lockout_policy.go b/internal/query/lockout_policy.go index be4b162785..078c743413 100644 --- a/internal/query/lockout_policy.go +++ b/internal/query/lockout_policy.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -98,7 +97,7 @@ func (q *Queries) LockoutPolicyByOrg(ctx context.Context, shouldTriggerBulk bool LockoutColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } - stmt, scan := prepareLockoutPolicyQuery(ctx, q.client) + stmt, scan := prepareLockoutPolicyQuery() query, args, err := stmt.Where( sq.And{ eq, @@ -124,7 +123,7 @@ func (q *Queries) DefaultLockoutPolicy(ctx context.Context) (policy *LockoutPoli ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareLockoutPolicyQuery(ctx, q.client) + stmt, scan := prepareLockoutPolicyQuery() query, args, err := stmt.Where(sq.Eq{ LockoutColID.identifier(): authz.GetInstance(ctx).InstanceID(), LockoutColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -142,7 +141,7 @@ func (q *Queries) DefaultLockoutPolicy(ctx context.Context) (policy *LockoutPoli return policy, err } -func prepareLockoutPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*LockoutPolicy, error)) { +func prepareLockoutPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LockoutPolicy, error)) { return sq.Select( LockoutColID.identifier(), LockoutColSequence.identifier(), @@ -155,7 +154,7 @@ func prepareLockoutPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sele LockoutColIsDefault.identifier(), LockoutColState.identifier(), ). - From(lockoutTable.identifier() + db.Timetravel(call.Took(ctx))). + From(lockoutTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*LockoutPolicy, error) { policy := new(LockoutPolicy) diff --git a/internal/query/lockout_policy_test.go b/internal/query/lockout_policy_test.go index 2805ef8fdc..0c0a9f04eb 100644 --- a/internal/query/lockout_policy_test.go +++ b/internal/query/lockout_policy_test.go @@ -23,8 +23,7 @@ var ( ` projections.lockout_policies3.max_otp_attempts,` + ` projections.lockout_policies3.is_default,` + ` projections.lockout_policies3.state` + - ` FROM projections.lockout_policies3` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.lockout_policies3` prepareLockoutPolicyCols = []string{ "id", @@ -123,7 +122,7 @@ func Test_LockoutPolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/login_policy.go b/internal/query/login_policy.go index 5ab54cfa55..946dbb04de 100644 --- a/internal/query/login_policy.go +++ b/internal/query/login_policy.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -183,7 +182,7 @@ func (q *Queries) LoginPolicyByID(ctx context.Context, shouldTriggerBulk bool, o eq[LoginPolicyColumnOwnerRemoved.identifier()] = false } - query, scan := prepareLoginPolicyQuery(ctx, q.client) + query, scan := prepareLoginPolicyQuery() stmt, args, err := query.Where( sq.And{ eq, @@ -219,7 +218,7 @@ func (q *Queries) DefaultLoginPolicy(ctx context.Context) (policy *LoginPolicy, ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginPolicyQuery(ctx, q.client) + query, scan := prepareLoginPolicyQuery() stmt, args, err := query.Where(sq.Eq{ LoginPolicyColumnOrgID.identifier(): authz.GetInstance(ctx).InstanceID(), LoginPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -242,7 +241,7 @@ func (q *Queries) SecondFactorsByOrg(ctx context.Context, orgID string) (factors ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginPolicy2FAsQuery(ctx, q.client) + query, scan := prepareLoginPolicy2FAsQuery() stmt, args, err := query.Where( sq.And{ sq.Eq{ @@ -278,7 +277,7 @@ func (q *Queries) DefaultSecondFactors(ctx context.Context) (factors *SecondFact ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginPolicy2FAsQuery(ctx, q.client) + query, scan := prepareLoginPolicy2FAsQuery() stmt, args, err := query.Where(sq.Eq{ LoginPolicyColumnOrgID.identifier(): authz.GetInstance(ctx).InstanceID(), LoginPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -302,7 +301,7 @@ func (q *Queries) MultiFactorsByOrg(ctx context.Context, orgID string) (factors ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginPolicyMFAsQuery(ctx, q.client) + query, scan := prepareLoginPolicyMFAsQuery() stmt, args, err := query.Where( sq.And{ sq.Eq{ @@ -338,7 +337,7 @@ func (q *Queries) DefaultMultiFactors(ctx context.Context) (factors *MultiFactor ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginPolicyMFAsQuery(ctx, q.client) + query, scan := prepareLoginPolicyMFAsQuery() stmt, args, err := query.Where(sq.Eq{ LoginPolicyColumnOrgID.identifier(): authz.GetInstance(ctx).InstanceID(), LoginPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -358,7 +357,7 @@ func (q *Queries) DefaultMultiFactors(ctx context.Context) (factors *MultiFactor return factors, err } -func prepareLoginPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*LoginPolicy, error)) { +func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Rows) (*LoginPolicy, error)) { return sq.Select( LoginPolicyColumnOrgID.identifier(), LoginPolicyColumnCreationDate.identifier(), @@ -384,7 +383,7 @@ func prepareLoginPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Select LoginPolicyColumnMFAInitSkipLifetime.identifier(), LoginPolicyColumnSecondFactorCheckLifetime.identifier(), LoginPolicyColumnMultiFactorCheckLifetime.identifier(), - ).From(loginPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(loginPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*LoginPolicy, error) { p := new(LoginPolicy) @@ -428,10 +427,10 @@ func prepareLoginPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Select } } -func prepareLoginPolicy2FAsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*SecondFactors, error)) { +func prepareLoginPolicy2FAsQuery() (sq.SelectBuilder, func(*sql.Row) (*SecondFactors, error)) { return sq.Select( LoginPolicyColumnSecondFactors.identifier(), - ).From(loginPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(loginPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SecondFactors, error) { p := new(SecondFactors) @@ -450,10 +449,10 @@ func prepareLoginPolicy2FAsQuery(ctx context.Context, db prepareDatabase) (sq.Se } } -func prepareLoginPolicyMFAsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*MultiFactors, error)) { +func prepareLoginPolicyMFAsQuery() (sq.SelectBuilder, func(*sql.Row) (*MultiFactors, error)) { return sq.Select( LoginPolicyColumnMultiFactors.identifier(), - ).From(loginPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(loginPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*MultiFactors, error) { p := new(MultiFactors) diff --git a/internal/query/login_policy_test.go b/internal/query/login_policy_test.go index f64c94e275..792f30517a 100644 --- a/internal/query/login_policy_test.go +++ b/internal/query/login_policy_test.go @@ -39,8 +39,7 @@ var ( ` projections.login_policies5.mfa_init_skip_lifetime,` + ` projections.login_policies5.second_factor_check_lifetime,` + ` projections.login_policies5.multi_factor_check_lifetime` + - ` FROM projections.login_policies5` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.login_policies5` loginPolicyCols = []string{ "aggregate_id", "creation_date", @@ -69,15 +68,13 @@ var ( } prepareLoginPolicy2FAsStmt = `SELECT projections.login_policies5.second_factors` + - ` FROM projections.login_policies5` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.login_policies5` prepareLoginPolicy2FAsCols = []string{ "second_factors", } prepareLoginPolicyMFAsStmt = `SELECT projections.login_policies5.multi_factors` + - ` FROM projections.login_policies5` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.login_policies5` prepareLoginPolicyMFAsCols = []string{ "multi_factors", } @@ -331,7 +328,7 @@ func Test_LoginPolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/mail_template.go b/internal/query/mail_template.go index 9d5ff83162..518ab7aec5 100644 --- a/internal/query/mail_template.go +++ b/internal/query/mail_template.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -74,7 +73,7 @@ func (q *Queries) MailTemplateByOrg(ctx context.Context, orgID string, withOwner ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareMailTemplateQuery(ctx, q.client) + stmt, scan := prepareMailTemplateQuery() eq := sq.Eq{MailTemplateColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} if !withOwnerRemoved { eq[MailTemplateColOwnerRemoved.identifier()] = false @@ -104,7 +103,7 @@ func (q *Queries) DefaultMailTemplate(ctx context.Context) (template *MailTempla ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareMailTemplateQuery(ctx, q.client) + stmt, scan := prepareMailTemplateQuery() query, args, err := stmt.Where(sq.Eq{ MailTemplateColAggregateID.identifier(): authz.GetInstance(ctx).InstanceID(), MailTemplateColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -122,7 +121,7 @@ func (q *Queries) DefaultMailTemplate(ctx context.Context) (template *MailTempla return template, err } -func prepareMailTemplateQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*MailTemplate, error)) { +func prepareMailTemplateQuery() (sq.SelectBuilder, func(*sql.Row) (*MailTemplate, error)) { return sq.Select( MailTemplateColAggregateID.identifier(), MailTemplateColSequence.identifier(), @@ -132,7 +131,7 @@ func prepareMailTemplateQuery(ctx context.Context, db prepareDatabase) (sq.Selec MailTemplateColIsDefault.identifier(), MailTemplateColState.identifier(), ). - From(mailTemplateTable.identifier() + db.Timetravel(call.Took(ctx))). + From(mailTemplateTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*MailTemplate, error) { policy := new(MailTemplate) diff --git a/internal/query/message_text.go b/internal/query/message_text.go index cb524d289a..3d9a2c33b3 100644 --- a/internal/query/message_text.go +++ b/internal/query/message_text.go @@ -6,7 +6,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "os" "time" @@ -15,7 +15,6 @@ import ( "sigs.k8s.io/yaml" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query/projection" @@ -131,7 +130,7 @@ func (q *Queries) DefaultMessageText(ctx context.Context) (text *MessageText, er ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareMessageTextQuery(ctx, q.client) + stmt, scan := prepareMessageTextQuery() query, args, err := stmt.Where(sq.Eq{ MessageTextColAggregateID.identifier(): authz.GetInstance(ctx).InstanceID(), MessageTextColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -167,7 +166,7 @@ func (q *Queries) CustomMessageTextByTypeAndLanguage(ctx context.Context, aggreg ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareMessageTextQuery(ctx, q.client) + stmt, scan := prepareMessageTextQuery() eq := sq.Eq{ MessageTextColLanguage.identifier(): language, MessageTextColType.identifier(): messageType, @@ -249,7 +248,7 @@ func (q *Queries) readNotificationTextMessages(ctx context.Context, language str return contents, nil } -func prepareMessageTextQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*MessageText, error)) { +func prepareMessageTextQuery() (sq.SelectBuilder, func(*sql.Row) (*MessageText, error)) { return sq.Select( MessageTextColAggregateID.identifier(), MessageTextColSequence.identifier(), @@ -266,7 +265,7 @@ func prepareMessageTextQuery(ctx context.Context, db prepareDatabase) (sq.Select MessageTextColButtonText.identifier(), MessageTextColFooter.identifier(), ). - From(messageTextTable.identifier() + db.Timetravel(call.Took(ctx))). + From(messageTextTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*MessageText, error) { msg := new(MessageText) @@ -320,7 +319,7 @@ func (q *Queries) readTranslationFile(namespace i18n.Namespace, filename string) if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-93njw", "Errors.TranslationFile.ReadError") } - contents, err := ioutil.ReadAll(r) + contents, err := io.ReadAll(r) if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-l0fse", "Errors.TranslationFile.ReadError") } diff --git a/internal/query/message_text_test.go b/internal/query/message_text_test.go index 09df5dcd83..4e78f4813d 100644 --- a/internal/query/message_text_test.go +++ b/internal/query/message_text_test.go @@ -29,8 +29,7 @@ var ( ` projections.message_texts2.text,` + ` projections.message_texts2.button_text,` + ` projections.message_texts2.footer_text` + - ` FROM projections.message_texts2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.message_texts2` prepareMessgeTextCols = []string{ "aggregate_id", "sequence", @@ -140,7 +139,7 @@ func Test_MessageTextPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/milestone.go b/internal/query/milestone.go index 631020e393..f6d2c47de0 100644 --- a/internal/query/milestone.go +++ b/internal/query/milestone.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/milestone" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -68,7 +67,7 @@ var ( func (q *Queries) SearchMilestones(ctx context.Context, instanceIDs []string, queries *MilestonesSearchQueries) (milestones *Milestones, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareMilestonesQuery(ctx, q.client) + query, scan := prepareMilestonesQuery() if len(instanceIDs) == 0 { instanceIDs = []string{authz.GetInstance(ctx).InstanceID()} } @@ -93,7 +92,7 @@ func (q *Queries) SearchMilestones(ctx context.Context, instanceIDs []string, qu return milestones, err } -func prepareMilestonesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Milestones, error)) { +func prepareMilestonesQuery() (sq.SelectBuilder, func(*sql.Rows) (*Milestones, error)) { return sq.Select( MilestoneInstanceIDColID.identifier(), InstanceDomainDomainCol.identifier(), @@ -102,7 +101,7 @@ func prepareMilestonesQuery(ctx context.Context, db prepareDatabase) (sq.SelectB MilestoneTypeColID.identifier(), countColumn.identifier(), ). - From(milestonesTable.identifier() + db.Timetravel(call.Took(ctx))). + From(milestonesTable.identifier()). LeftJoin(join(InstanceDomainInstanceIDCol, MilestoneInstanceIDColID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Milestones, error) { diff --git a/internal/query/milestone_test.go b/internal/query/milestone_test.go index ee99474ec2..027ebca48c 100644 --- a/internal/query/milestone_test.go +++ b/internal/query/milestone_test.go @@ -17,7 +17,7 @@ var ( projections.milestones3.last_pushed_date, projections.milestones3.type, COUNT(*) OVER () - FROM projections.milestones3 AS OF SYSTEM TIME '-1 ms' + FROM projections.milestones3 LEFT JOIN projections.instance_domains ON projections.milestones3.instance_id = projections.instance_domains.instance_id `) @@ -184,7 +184,7 @@ func Test_MilestonesPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/notification_policy.go b/internal/query/notification_policy.go index f3878e7987..45779762d9 100644 --- a/internal/query/notification_policy.go +++ b/internal/query/notification_policy.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -93,7 +92,7 @@ func (q *Queries) NotificationPolicyByOrg(ctx context.Context, shouldTriggerBulk if !withOwnerRemoved { eq[NotificationPolicyColOwnerRemoved.identifier()] = false } - stmt, scan := prepareNotificationPolicyQuery(ctx, q.client) + stmt, scan := prepareNotificationPolicyQuery() query, args, err := stmt.Where( sq.And{ eq, @@ -127,7 +126,7 @@ func (q *Queries) DefaultNotificationPolicy(ctx context.Context, shouldTriggerBu } } - stmt, scan := prepareNotificationPolicyQuery(ctx, q.client) + stmt, scan := prepareNotificationPolicyQuery() query, args, err := stmt.Where(sq.Eq{ NotificationPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID(), NotificationPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -145,7 +144,7 @@ func (q *Queries) DefaultNotificationPolicy(ctx context.Context, shouldTriggerBu return policy, err } -func prepareNotificationPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*NotificationPolicy, error)) { +func prepareNotificationPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*NotificationPolicy, error)) { return sq.Select( NotificationPolicyColID.identifier(), NotificationPolicyColSequence.identifier(), @@ -156,7 +155,7 @@ func prepareNotificationPolicyQuery(ctx context.Context, db prepareDatabase) (sq NotificationPolicyColIsDefault.identifier(), NotificationPolicyColState.identifier(), ). - From(notificationPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + From(notificationPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*NotificationPolicy, error) { policy := new(NotificationPolicy) diff --git a/internal/query/notification_policy_test.go b/internal/query/notification_policy_test.go index d755bdc544..bbb40d4e5b 100644 --- a/internal/query/notification_policy_test.go +++ b/internal/query/notification_policy_test.go @@ -21,8 +21,7 @@ var ( ` projections.notification_policies.password_change,` + ` projections.notification_policies.is_default,` + ` projections.notification_policies.state` + - ` FROM projections.notification_policies` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` FROM projections.notification_policies`) notificationPolicyCols = []string{ "id", "sequence", @@ -114,7 +113,7 @@ func Test_NotificationPolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/notification_provider.go b/internal/query/notification_provider.go index b2038c603d..fa48e42c9b 100644 --- a/internal/query/notification_provider.go +++ b/internal/query/notification_provider.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -74,7 +73,7 @@ func (q *Queries) NotificationProviderByIDAndType(ctx context.Context, aggID str ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareDebugNotificationProviderQuery(ctx, q.client) + query, scan := prepareDebugNotificationProviderQuery() stmt, args, err := query.Where( sq.And{ sq.Eq{NotificationProviderColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}, @@ -97,7 +96,7 @@ func (q *Queries) NotificationProviderByIDAndType(ctx context.Context, aggID str return provider, err } -func prepareDebugNotificationProviderQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*DebugNotificationProvider, error)) { +func prepareDebugNotificationProviderQuery() (sq.SelectBuilder, func(*sql.Row) (*DebugNotificationProvider, error)) { return sq.Select( NotificationProviderColumnAggID.identifier(), NotificationProviderColumnCreationDate.identifier(), @@ -107,7 +106,7 @@ func prepareDebugNotificationProviderQuery(ctx context.Context, db prepareDataba NotificationProviderColumnState.identifier(), NotificationProviderColumnType.identifier(), NotificationProviderColumnCompact.identifier(), - ).From(notificationProviderTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(notificationProviderTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*DebugNotificationProvider, error) { p := new(DebugNotificationProvider) diff --git a/internal/query/notification_provider_test.go b/internal/query/notification_provider_test.go index 2fce31e118..a2c88ccbcb 100644 --- a/internal/query/notification_provider_test.go +++ b/internal/query/notification_provider_test.go @@ -21,8 +21,7 @@ var ( ` projections.notification_providers.state,` + ` projections.notification_providers.provider_type,` + ` projections.notification_providers.compact` + - ` FROM projections.notification_providers` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.notification_providers` prepareNotificationProviderCols = []string{ "aggregate_id", "creation_date", @@ -114,7 +113,7 @@ func Test_NotificationProviderPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/oidc_client_test.go b/internal/query/oidc_client_test.go index 25e069da85..826e5071db 100644 --- a/internal/query/oidc_client_test.go +++ b/internal/query/oidc_client_test.go @@ -268,8 +268,7 @@ low2kyJov38V4Uk2I8kuXpLcnrpw5Tio2ooiUE27b0vHZqBKOei9Uo88qCrn3EKx execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") diff --git a/internal/query/oidc_settings.go b/internal/query/oidc_settings.go index 32cbc32429..bdd21cfd15 100644 --- a/internal/query/oidc_settings.go +++ b/internal/query/oidc_settings.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -79,7 +78,7 @@ func (q *Queries) OIDCSettingsByAggID(ctx context.Context, aggregateID string) ( ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareOIDCSettingsQuery(ctx, q.client) + stmt, scan := prepareOIDCSettingsQuery() query, args, err := stmt.Where(sq.Eq{ OIDCSettingsColumnAggregateID.identifier(): aggregateID, OIDCSettingsColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -95,7 +94,7 @@ func (q *Queries) OIDCSettingsByAggID(ctx context.Context, aggregateID string) ( return settings, err } -func prepareOIDCSettingsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*OIDCSettings, error)) { +func prepareOIDCSettingsQuery() (sq.SelectBuilder, func(*sql.Row) (*OIDCSettings, error)) { return sq.Select( OIDCSettingsColumnAggregateID.identifier(), OIDCSettingsColumnCreationDate.identifier(), @@ -106,7 +105,7 @@ func prepareOIDCSettingsQuery(ctx context.Context, db prepareDatabase) (sq.Selec OIDCSettingsColumnIdTokenLifetime.identifier(), OIDCSettingsColumnRefreshTokenIdleExpiration.identifier(), OIDCSettingsColumnRefreshTokenExpiration.identifier()). - From(oidcSettingsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(oidcSettingsTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*OIDCSettings, error) { oidcSettings := new(OIDCSettings) diff --git a/internal/query/oidc_settings_test.go b/internal/query/oidc_settings_test.go index bdb5cb96ec..625c16f34c 100644 --- a/internal/query/oidc_settings_test.go +++ b/internal/query/oidc_settings_test.go @@ -22,8 +22,7 @@ var ( ` projections.oidc_settings2.id_token_lifetime,` + ` projections.oidc_settings2.refresh_token_idle_expiration,` + ` projections.oidc_settings2.refresh_token_expiration` + - ` FROM projections.oidc_settings2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.oidc_settings2` prepareOIDCSettingsCols = []string{ "aggregate_id", "creation_date", @@ -118,7 +117,7 @@ func Test_OIDCConfigsPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/org.go b/internal/query/org.go index e5bfc5140f..b1f5eaea02 100644 --- a/internal/query/org.go +++ b/internal/query/org.go @@ -11,7 +11,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" domain_pkg "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/feature" @@ -164,7 +163,7 @@ func (q *Queries) oldOrgByID(ctx context.Context, shouldTriggerBulk bool, id str traceSpan.EndWithError(err) } - stmt, scan := prepareOrgQuery(ctx, q.client) + stmt, scan := prepareOrgQuery() query, args, err := stmt.Where(sq.Eq{ OrgColumnID.identifier(): id, OrgColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -189,7 +188,7 @@ func (q *Queries) OrgByPrimaryDomain(ctx context.Context, domain string) (org *O return org, nil } - stmt, scan := prepareOrgQuery(ctx, q.client) + stmt, scan := prepareOrgQuery() query, args, err := stmt.Where(sq.Eq{ OrgColumnDomain.identifier(): domain, OrgColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -213,7 +212,7 @@ func (q *Queries) OrgByVerifiedDomain(ctx context.Context, domain string) (org * ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareOrgWithDomainsQuery(ctx, q.client) + stmt, scan := prepareOrgWithDomainsQuery() query, args, err := stmt.Where(sq.Eq{ OrgDomainDomainCol.identifier(): domain, OrgDomainIsVerifiedCol.identifier(): true, @@ -237,7 +236,7 @@ func (q *Queries) IsOrgUnique(ctx context.Context, name, domain string) (isUniqu if name == "" && domain == "" { return false, zerrors.ThrowInvalidArgument(nil, "QUERY-DGqfd", "Errors.Query.InvalidRequest") } - query, scan := prepareOrgUniqueQuery(ctx, q.client) + query, scan := prepareOrgUniqueQuery() stmt, args, err := query.Where( sq.And{ sq.Eq{ @@ -298,7 +297,7 @@ func (q *Queries) searchOrgs(ctx context.Context, queries *OrgSearchQueries) (or ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareOrgsQuery(ctx, q.client) + query, scan := prepareOrgsQuery() stmt, args, err := queries.toQuery(query). Where(sq.And{ sq.Eq{ @@ -361,7 +360,7 @@ func NewOrgIDsSearchQuery(ids ...string) (SearchQuery, error) { return NewListQuery(OrgColumnID, list, ListIn) } -func prepareOrgsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Orgs, error)) { +func prepareOrgsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Orgs, error)) { return sq.Select( OrgColumnID.identifier(), OrgColumnCreationDate.identifier(), @@ -372,7 +371,7 @@ func prepareOrgsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder OrgColumnName.identifier(), OrgColumnDomain.identifier(), countColumn.identifier()). - From(orgsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(orgsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Orgs, error) { orgs := make([]*Org, 0) @@ -409,42 +408,7 @@ func prepareOrgsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder } } -func prepareOrgQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { - return sq.Select( - OrgColumnID.identifier(), - OrgColumnCreationDate.identifier(), - OrgColumnChangeDate.identifier(), - OrgColumnResourceOwner.identifier(), - OrgColumnState.identifier(), - OrgColumnSequence.identifier(), - OrgColumnName.identifier(), - OrgColumnDomain.identifier(), - ). - From(orgsTable.identifier() + db.Timetravel(call.Took(ctx))). - PlaceholderFormat(sq.Dollar), - func(row *sql.Row) (*Org, error) { - o := new(Org) - err := row.Scan( - &o.ID, - &o.CreationDate, - &o.ChangeDate, - &o.ResourceOwner, - &o.State, - &o.Sequence, - &o.Name, - &o.Domain, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, zerrors.ThrowNotFound(err, "QUERY-iTTGJ", "Errors.Org.NotFound") - } - return nil, zerrors.ThrowInternal(err, "QUERY-pWS5H", "Errors.Internal") - } - return o, nil - } -} - -func prepareOrgWithDomainsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { +func prepareOrgQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { return sq.Select( OrgColumnID.identifier(), OrgColumnCreationDate.identifier(), @@ -456,7 +420,6 @@ func prepareOrgWithDomainsQuery(ctx context.Context, db prepareDatabase) (sq.Sel OrgColumnDomain.identifier(), ). From(orgsTable.identifier()). - LeftJoin(join(OrgDomainOrgIDCol, OrgColumnID) + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Org, error) { o := new(Org) @@ -480,10 +443,46 @@ func prepareOrgWithDomainsQuery(ctx context.Context, db prepareDatabase) (sq.Sel } } -func prepareOrgUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (bool, error)) { +func prepareOrgWithDomainsQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { + return sq.Select( + OrgColumnID.identifier(), + OrgColumnCreationDate.identifier(), + OrgColumnChangeDate.identifier(), + OrgColumnResourceOwner.identifier(), + OrgColumnState.identifier(), + OrgColumnSequence.identifier(), + OrgColumnName.identifier(), + OrgColumnDomain.identifier(), + ). + From(orgsTable.identifier()). + LeftJoin(join(OrgDomainOrgIDCol, OrgColumnID)). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*Org, error) { + o := new(Org) + err := row.Scan( + &o.ID, + &o.CreationDate, + &o.ChangeDate, + &o.ResourceOwner, + &o.State, + &o.Sequence, + &o.Name, + &o.Domain, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-iTTGJ", "Errors.Org.NotFound") + } + return nil, zerrors.ThrowInternal(err, "QUERY-pWS5H", "Errors.Internal") + } + return o, nil + } +} + +func prepareOrgUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) { return sq.Select(uniqueColumn.identifier()). From(orgsTable.identifier()). - LeftJoin(join(OrgDomainOrgIDCol, OrgColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(OrgDomainOrgIDCol, OrgColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (isUnique bool, err error) { err = row.Scan(&isUnique) diff --git a/internal/query/org_domain.go b/internal/query/org_domain.go index 595ba897d0..ed0dba9c17 100644 --- a/internal/query/org_domain.go +++ b/internal/query/org_domain.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -60,7 +59,7 @@ func (q *Queries) SearchOrgDomains(ctx context.Context, queries *OrgDomainSearch ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareDomainsQuery(ctx, q.client) + query, scan := prepareDomainsQuery() eq := sq.Eq{OrgDomainInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} if !withOwnerRemoved { eq[OrgDomainOwnerRemovedCol.identifier()] = false @@ -82,7 +81,7 @@ func (q *Queries) SearchOrgDomains(ctx context.Context, queries *OrgDomainSearch return domains, err } -func prepareDomainsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Domains, error)) { +func prepareDomainsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Domains, error)) { return sq.Select( OrgDomainCreationDateCol.identifier(), OrgDomainChangeDateCol.identifier(), @@ -93,7 +92,7 @@ func prepareDomainsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil OrgDomainIsPrimaryCol.identifier(), OrgDomainValidationTypeCol.identifier(), countColumn.identifier(), - ).From(orgDomainsTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(orgDomainsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Domains, error) { domains := make([]*Domain, 0) diff --git a/internal/query/org_domain_test.go b/internal/query/org_domain_test.go index 5757eda657..6668528241 100644 --- a/internal/query/org_domain_test.go +++ b/internal/query/org_domain_test.go @@ -21,8 +21,7 @@ var ( ` projections.org_domains2.is_primary,` + ` projections.org_domains2.validation_type,` + ` COUNT(*) OVER ()` + - ` FROM projections.org_domains2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.org_domains2` prepareOrgDomainsCols = []string{ "id", "creation_date", @@ -177,7 +176,7 @@ func Test_OrgDomainPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/org_member.go b/internal/query/org_member.go index 4daa31d341..a85c5d5f6a 100644 --- a/internal/query/org_member.go +++ b/internal/query/org_member.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -73,7 +72,7 @@ func (q *Queries) OrgMembers(ctx context.Context, queries *OrgMembersQuery) (mem ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareOrgMembersQuery(ctx, q.client) + query, scan := prepareOrgMembersQuery() eq := sq.Eq{OrgMemberInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -97,7 +96,7 @@ func (q *Queries) OrgMembers(ctx context.Context, queries *OrgMembersQuery) (mem return members, err } -func prepareOrgMembersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { +func prepareOrgMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { return sq.Select( OrgMemberCreationDate.identifier(), OrgMemberChangeDate.identifier(), @@ -119,7 +118,7 @@ func prepareOrgMembersQuery(ctx context.Context, db prepareDatabase) (sq.SelectB LeftJoin(join(HumanUserIDCol, OrgMemberUserID)). LeftJoin(join(MachineUserIDCol, OrgMemberUserID)). LeftJoin(join(UserIDCol, OrgMemberUserID)). - LeftJoin(join(LoginNameUserIDCol, OrgMemberUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, OrgMemberUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/org_member_test.go b/internal/query/org_member_test.go index 8433c338ee..cb0b64d55f 100644 --- a/internal/query/org_member_test.go +++ b/internal/query/org_member_test.go @@ -43,7 +43,6 @@ var ( "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id " + "AND members.instance_id = projections.login_names3.instance_id " + - "AS OF SYSTEM TIME '-1 ms' " + "WHERE projections.login_names3.is_primary = $1") orgMembersColumns = []string{ "creation_date", @@ -299,7 +298,7 @@ func Test_OrgMemberPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/org_metadata.go b/internal/query/org_metadata.go index 1ce95e2880..84b204de2b 100644 --- a/internal/query/org_metadata.go +++ b/internal/query/org_metadata.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -90,7 +89,7 @@ func (q *Queries) GetOrgMetadataByKey(ctx context.Context, shouldTriggerBulk boo traceSpan.EndWithError(err) } - query, scan := prepareOrgMetadataQuery(ctx, q.client) + query, scan := prepareOrgMetadataQuery() for _, q := range queries { query = q.toQuery(query) } @@ -131,7 +130,7 @@ func (q *Queries) SearchOrgMetadata(ctx context.Context, shouldTriggerBulk bool, if !withOwnerRemoved { eq[OrgMetadataOwnerRemovedCol.identifier()] = false } - query, scan := prepareOrgMetadataListQuery(ctx, q.client) + query, scan := prepareOrgMetadataListQuery() stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-Egbld", "Errors.Query.SQLStatment") @@ -174,7 +173,7 @@ func NewOrgMetadataKeySearchQuery(value string, comparison TextComparison) (Sear return NewTextQuery(OrgMetadataKeyCol, value, comparison) } -func prepareOrgMetadataQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*OrgMetadata, error)) { +func prepareOrgMetadataQuery() (sq.SelectBuilder, func(*sql.Row) (*OrgMetadata, error)) { return sq.Select( OrgMetadataCreationDateCol.identifier(), OrgMetadataChangeDateCol.identifier(), @@ -183,7 +182,7 @@ func prepareOrgMetadataQuery(ctx context.Context, db prepareDatabase) (sq.Select OrgMetadataKeyCol.identifier(), OrgMetadataValueCol.identifier(), ). - From(orgMetadataTable.identifier() + db.Timetravel(call.Took(ctx))). + From(orgMetadataTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*OrgMetadata, error) { m := new(OrgMetadata) @@ -206,7 +205,7 @@ func prepareOrgMetadataQuery(ctx context.Context, db prepareDatabase) (sq.Select } } -func prepareOrgMetadataListQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*OrgMetadataList, error)) { +func prepareOrgMetadataListQuery() (sq.SelectBuilder, func(*sql.Rows) (*OrgMetadataList, error)) { return sq.Select( OrgMetadataCreationDateCol.identifier(), OrgMetadataChangeDateCol.identifier(), @@ -215,7 +214,7 @@ func prepareOrgMetadataListQuery(ctx context.Context, db prepareDatabase) (sq.Se OrgMetadataKeyCol.identifier(), OrgMetadataValueCol.identifier(), countColumn.identifier()). - From(orgMetadataTable.identifier() + db.Timetravel(call.Took(ctx))). + From(orgMetadataTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*OrgMetadataList, error) { metadata := make([]*OrgMetadata, 0) diff --git a/internal/query/org_metadata_test.go b/internal/query/org_metadata_test.go index 0225ef1c2a..666fddd0fd 100644 --- a/internal/query/org_metadata_test.go +++ b/internal/query/org_metadata_test.go @@ -18,8 +18,7 @@ var ( ` projections.org_metadata2.sequence,` + ` projections.org_metadata2.key,` + ` projections.org_metadata2.value` + - ` FROM projections.org_metadata2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.org_metadata2` orgMetadataCols = []string{ "creation_date", "change_date", @@ -35,8 +34,7 @@ var ( ` projections.org_metadata2.key,` + ` projections.org_metadata2.value,` + ` COUNT(*) OVER ()` + - ` FROM projections.org_metadata2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.org_metadata2` orgMetadataListCols = []string{ "creation_date", "change_date", @@ -244,7 +242,7 @@ func Test_OrgMetadataPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/org_test.go b/internal/query/org_test.go index db41f9ffd1..d704d2901a 100644 --- a/internal/query/org_test.go +++ b/internal/query/org_test.go @@ -19,7 +19,7 @@ import ( ) var ( - orgUniqueQuery = "SELECT COUNT(*) = 0 FROM projections.orgs1 LEFT JOIN projections.org_domains2 ON projections.orgs1.id = projections.org_domains2.org_id AND projections.orgs1.instance_id = projections.org_domains2.instance_id AS OF SYSTEM TIME '-1 ms' WHERE (projections.org_domains2.is_verified = $1 AND projections.orgs1.instance_id = $2 AND (projections.org_domains2.domain ILIKE $3 OR projections.orgs1.name ILIKE $4) AND projections.orgs1.org_state <> $5)" + orgUniqueQuery = "SELECT COUNT(*) = 0 FROM projections.orgs1 LEFT JOIN projections.org_domains2 ON projections.orgs1.id = projections.org_domains2.org_id AND projections.orgs1.instance_id = projections.org_domains2.instance_id WHERE (projections.org_domains2.is_verified = $1 AND projections.orgs1.instance_id = $2 AND (projections.org_domains2.domain ILIKE $3 OR projections.orgs1.name ILIKE $4) AND projections.orgs1.org_state <> $5)" orgUniqueCols = []string{"is_unique"} prepareOrgsQueryStmt = `SELECT projections.orgs1.id,` + @@ -31,8 +31,7 @@ var ( ` projections.orgs1.name,` + ` projections.orgs1.primary_domain,` + ` COUNT(*) OVER ()` + - ` FROM projections.orgs1` + - ` AS OF SYSTEM TIME '-1 ms' ` + ` FROM projections.orgs1` prepareOrgsQueryCols = []string{ "id", "creation_date", @@ -53,8 +52,7 @@ var ( ` projections.orgs1.sequence,` + ` projections.orgs1.name,` + ` projections.orgs1.primary_domain` + - ` FROM projections.orgs1` + - ` AS OF SYSTEM TIME '-1 ms' ` + ` FROM projections.orgs1` prepareOrgQueryCols = []string{ "id", "creation_date", @@ -68,8 +66,7 @@ var ( prepareOrgUniqueStmt = `SELECT COUNT(*) = 0` + ` FROM projections.orgs1` + - ` LEFT JOIN projections.org_domains2 ON projections.orgs1.id = projections.org_domains2.org_id AND projections.orgs1.instance_id = projections.org_domains2.instance_id` + - ` AS OF SYSTEM TIME '-1 ms' ` + ` LEFT JOIN projections.org_domains2 ON projections.orgs1.id = projections.org_domains2.org_id AND projections.orgs1.instance_id = projections.org_domains2.instance_id` prepareOrgUniqueCols = []string{ "count", } @@ -330,7 +327,7 @@ func Test_OrgPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -421,8 +418,7 @@ func TestQueries_IsOrgUnique(t *testing.T) { t.Run(tt.name, func(t *testing.T) { q := &Queries{ client: &database.DB{ - DB: client, - Database: new(prepareDB), + DB: client, }, } diff --git a/internal/query/password_age_policy.go b/internal/query/password_age_policy.go index 15b1b248c8..f5f0491d7b 100644 --- a/internal/query/password_age_policy.go +++ b/internal/query/password_age_policy.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -97,7 +96,7 @@ func (q *Queries) PasswordAgePolicyByOrg(ctx context.Context, shouldTriggerBulk if !withOwnerRemoved { eq[PasswordAgeColOwnerRemoved.identifier()] = false } - stmt, scan := preparePasswordAgePolicyQuery(ctx, q.client) + stmt, scan := preparePasswordAgePolicyQuery() query, args, err := stmt.Where( sq.And{ eq, @@ -130,7 +129,7 @@ func (q *Queries) DefaultPasswordAgePolicy(ctx context.Context, shouldTriggerBul traceSpan.EndWithError(err) } - stmt, scan := preparePasswordAgePolicyQuery(ctx, q.client) + stmt, scan := preparePasswordAgePolicyQuery() query, args, err := stmt.Where(sq.Eq{ PasswordAgeColID.identifier(): authz.GetInstance(ctx).InstanceID(), }). @@ -147,7 +146,7 @@ func (q *Queries) DefaultPasswordAgePolicy(ctx context.Context, shouldTriggerBul return policy, err } -func preparePasswordAgePolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*PasswordAgePolicy, error)) { +func preparePasswordAgePolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*PasswordAgePolicy, error)) { return sq.Select( PasswordAgeColID.identifier(), PasswordAgeColSequence.identifier(), @@ -159,7 +158,7 @@ func preparePasswordAgePolicyQuery(ctx context.Context, db prepareDatabase) (sq. PasswordAgeColIsDefault.identifier(), PasswordAgeColState.identifier(), ). - From(passwordAgeTable.identifier() + db.Timetravel(call.Took(ctx))). + From(passwordAgeTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*PasswordAgePolicy, error) { policy := new(PasswordAgePolicy) diff --git a/internal/query/password_age_policy_test.go b/internal/query/password_age_policy_test.go index b140f82a06..f40acdb559 100644 --- a/internal/query/password_age_policy_test.go +++ b/internal/query/password_age_policy_test.go @@ -22,8 +22,7 @@ var ( ` projections.password_age_policies2.max_age_days,` + ` projections.password_age_policies2.is_default,` + ` projections.password_age_policies2.state` + - ` FROM projections.password_age_policies2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.password_age_policies2` preparePasswordAgePolicyCols = []string{ "id", "sequence", @@ -118,7 +117,7 @@ func Test_PasswordAgePolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/password_complexity_policy.go b/internal/query/password_complexity_policy.go index a895c98b75..fa0e5b2691 100644 --- a/internal/query/password_complexity_policy.go +++ b/internal/query/password_complexity_policy.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -49,7 +48,7 @@ func (q *Queries) PasswordComplexityPolicyByOrg(ctx context.Context, shouldTrigg if !withOwnerRemoved { eq[PasswordComplexityColOwnerRemoved.identifier()] = false } - stmt, scan := preparePasswordComplexityPolicyQuery(ctx, q.client) + stmt, scan := preparePasswordComplexityPolicyQuery() query, args, err := stmt.Where( sq.And{ eq, @@ -82,7 +81,7 @@ func (q *Queries) DefaultPasswordComplexityPolicy(ctx context.Context, shouldTri traceSpan.EndWithError(err) } - stmt, scan := preparePasswordComplexityPolicyQuery(ctx, q.client) + stmt, scan := preparePasswordComplexityPolicyQuery() query, args, err := stmt.Where(sq.Eq{ PasswordComplexityColID.identifier(): authz.GetInstance(ctx).InstanceID(), PasswordComplexityColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -163,7 +162,7 @@ var ( } ) -func preparePasswordComplexityPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*PasswordComplexityPolicy, error)) { +func preparePasswordComplexityPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*PasswordComplexityPolicy, error)) { return sq.Select( PasswordComplexityColID.identifier(), PasswordComplexityColSequence.identifier(), @@ -178,7 +177,7 @@ func preparePasswordComplexityPolicyQuery(ctx context.Context, db prepareDatabas PasswordComplexityColIsDefault.identifier(), PasswordComplexityColState.identifier(), ). - From(passwordComplexityTable.identifier() + db.Timetravel(call.Took(ctx))). + From(passwordComplexityTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*PasswordComplexityPolicy, error) { policy := new(PasswordComplexityPolicy) diff --git a/internal/query/password_complexity_policy_test.go b/internal/query/password_complexity_policy_test.go index ac471f3994..e5738049dd 100644 --- a/internal/query/password_complexity_policy_test.go +++ b/internal/query/password_complexity_policy_test.go @@ -25,8 +25,7 @@ var ( ` projections.password_complexity_policies2.has_symbol,` + ` projections.password_complexity_policies2.is_default,` + ` projections.password_complexity_policies2.state` + - ` FROM projections.password_complexity_policies2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.password_complexity_policies2` preparePasswordComplexityPolicyCols = []string{ "id", "sequence", @@ -130,7 +129,7 @@ func Test_PasswordComplexityPolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/permission.go b/internal/query/permission.go index aeda33e541..c52b491144 100644 --- a/internal/query/permission.go +++ b/internal/query/permission.go @@ -2,51 +2,74 @@ package query import ( "context" + "encoding/json" "fmt" sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/zerrors" ) const ( - // eventstore.permitted_orgs(instanceid text, userid text, perm text, filter_orgs text) - wherePermittedOrgsClause = "%s = ANY(eventstore.permitted_orgs(?, ?, ?, ?))" + // eventstore.permitted_orgs(instanceid text, userid text, system_user_perms JSONB, perm text filter_orgs text) + wherePermittedOrgsClause = "%s = ANY(eventstore.permitted_orgs(?, ?, ?, ?, ?))" wherePermittedOrgsOrCurrentUserClause = "(" + wherePermittedOrgsClause + " OR %s = ?" + ")" ) // wherePermittedOrgs sets a `WHERE` clause to the query that filters the orgs // for which the authenticated user has the requested permission for. // The user ID is taken from the context. -// // The `orgIDColumn` specifies the table column to which this filter must be applied, // and is typically the `resource_owner` column in ZITADEL. // We use full identifiers in the query builder so this function should be // called with something like `UserResourceOwnerCol.identifier()` for example. -func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, permission string) sq.SelectBuilder { - userID := authz.GetCtxData(ctx).UserID - logging.WithFields("permission_check_v2_flag", authz.GetFeatures(ctx).PermissionCheckV2, "org_id_column", orgIDColumn, "permission", permission, "user_id", userID).Debug("permitted orgs check used") +// func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, permission string) (sq.SelectBuilder, error) { +// userID := authz.GetCtxData(ctx).UserID +// logging.WithFields("permission_check_v2_flag", authz.GetFeatures(ctx).PermissionCheckV2, "org_id_column", orgIDColumn, "permission", permission, "user_id", userID).Debug("permitted orgs check used") - return query.Where( - fmt.Sprintf(wherePermittedOrgsClause, orgIDColumn), - authz.GetInstance(ctx).InstanceID(), - userID, - permission, - filterOrgIds, - ) -} +// systemUserPermissions := authz.GetSystemUserPermissions(ctx) +// var systemUserPermissionsJson []byte +// if systemUserPermissions != nil { +// var err error +// systemUserPermissionsJson, err = json.Marshal(systemUserPermissions) +// if err != nil { +// return query, err +// } +// } -func wherePermittedOrgsOrCurrentUser(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, userIdColum, permission string) sq.SelectBuilder { +// return query.Where( +// fmt.Sprintf(wherePermittedOrgsClause, orgIDColumn), +// authz.GetInstance(ctx).InstanceID(), +// userID, +// systemUserPermissionsJson, +// permission, +// filterOrgIds, +// ), nil +// } + +func wherePermittedOrgsOrCurrentUser(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, userIdColum, permission string) (sq.SelectBuilder, error) { userID := authz.GetCtxData(ctx).UserID logging.WithFields("permission_check_v2_flag", authz.GetFeatures(ctx).PermissionCheckV2, "org_id_column", orgIDColumn, "user_id_colum", userIdColum, "permission", permission, "user_id", userID).Debug("permitted orgs check used") + systemUserPermissions := authz.GetSystemUserPermissions(ctx) + var systemUserPermissionsJson []byte + if systemUserPermissions != nil { + var err error + systemUserPermissionsJson, err = json.Marshal(systemUserPermissions) + if err != nil { + return query, zerrors.ThrowInternal(err, "AUTHZ-HS4us", "Errors.Internal") + } + } + return query.Where( fmt.Sprintf(wherePermittedOrgsOrCurrentUserClause, orgIDColumn, userIdColum), authz.GetInstance(ctx).InstanceID(), userID, + systemUserPermissionsJson, permission, filterOrgIds, userID, - ) + ), nil } diff --git a/internal/query/prepare_test.go b/internal/query/prepare_test.go index f8cf31cdef..e243426260 100644 --- a/internal/query/prepare_test.go +++ b/internal/query/prepare_test.go @@ -1,7 +1,6 @@ package query import ( - "context" "database/sql" "database/sql/driver" "errors" @@ -32,7 +31,7 @@ var ( // func() (sq.SelectBuilder, func(*sql.Row) (*struct, error)) // expectedObject represents the return value of scan // sqlExpectation represents the query executed on the database -func assertPrepare(t *testing.T, prepareFunc, expectedObject interface{}, sqlExpectation sqlExpectation, isErr checkErr, prepareArgs ...reflect.Value) bool { +func assertPrepare(t *testing.T, prepareFunc, expectedObject any, sqlExpectation sqlExpectation, isErr checkErr, prepareArgs ...reflect.Value) bool { t.Helper() client, mock, err := sqlmock.New(sqlmock.ValueConverterOption(new(db_mock.TypeConverter))) @@ -243,9 +242,9 @@ func validateScan(scanType reflect.Type) error { return nil } -func execPrepare(prepare interface{}, args []reflect.Value) (builder sq.SelectBuilder, scan interface{}, err error) { +func execPrepare(prepare any, args []reflect.Value) (builder sq.SelectBuilder, scan interface{}, err error) { prepareVal := reflect.ValueOf(prepare) - if err := validatePrepare(prepareVal.Type()); err != nil { + if err := validatePrepare(prepareVal.Type(), len(args)); err != nil { return sq.SelectBuilder{}, nil, err } res := prepareVal.Call(args) @@ -253,12 +252,12 @@ func execPrepare(prepare interface{}, args []reflect.Value) (builder sq.SelectBu return res[0].Interface().(sq.SelectBuilder), res[1].Interface(), nil } -func validatePrepare(prepareType reflect.Type) error { +func validatePrepare(prepareType reflect.Type, numArgs int) error { if prepareType.Kind() != reflect.Func { return errors.New("prepare is not a function") } - if prepareType.NumIn() != 0 && prepareType.NumIn() != 2 { - return fmt.Errorf("prepare: invalid number of inputs: want: 0 or 2 got %d", prepareType.NumIn()) + if prepareType.NumIn() != numArgs { + return fmt.Errorf("prepare: invalid number of inputs: want: %d got %d", numArgs, prepareType.NumIn()) } if prepareType.NumOut() != 2 { return fmt.Errorf("prepare: invalid number of outputs: want: 2 got %d", prepareType.NumOut()) @@ -363,7 +362,7 @@ func TestValidatePrepare(t *testing.T) { }, { name: "correct", - t: reflect.TypeOf(func(context.Context, prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (interface{}, error)) { + t: reflect.TypeOf(func() (sq.SelectBuilder, func(*sql.Rows) (interface{}, error)) { log.Fatal("should not be executed") return sq.SelectBuilder{}, nil }), @@ -372,24 +371,10 @@ func TestValidatePrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := validatePrepare(tt.t) + err := validatePrepare(tt.t, 0) if (err != nil) != tt.expectErr { t.Errorf("unexpected err: %v", err) } }) } } - -type prepareDB struct{} - -const asOfSystemTime = " AS OF SYSTEM TIME '-1 ms' " - -func (*prepareDB) Timetravel(time.Duration) string { return asOfSystemTime } - -var defaultPrepareArgs = []reflect.Value{reflect.ValueOf(context.Background()), reflect.ValueOf(new(prepareDB))} - -func (*prepareDB) DatabaseName() string { return "db" } - -func (*prepareDB) Username() string { return "user" } - -func (*prepareDB) Type() string { return "type" } diff --git a/internal/query/privacy_policy.go b/internal/query/privacy_policy.go index 59394e92b1..e26948f478 100644 --- a/internal/query/privacy_policy.go +++ b/internal/query/privacy_policy.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -122,7 +121,7 @@ func (q *Queries) PrivacyPolicyByOrg(ctx context.Context, shouldTriggerBulk bool if !withOwnerRemoved { eq[PrivacyColOwnerRemoved.identifier()] = false } - stmt, scan := preparePrivacyPolicyQuery(ctx, q.client) + stmt, scan := preparePrivacyPolicyQuery() query, args, err := stmt.Where( sq.And{ eq, @@ -154,7 +153,7 @@ func (q *Queries) DefaultPrivacyPolicy(ctx context.Context, shouldTriggerBulk bo traceSpan.EndWithError(err) } - stmt, scan := preparePrivacyPolicyQuery(ctx, q.client) + stmt, scan := preparePrivacyPolicyQuery() query, args, err := stmt.Where(sq.Eq{ PrivacyColID.identifier(): authz.GetInstance(ctx).InstanceID(), PrivacyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -172,7 +171,7 @@ func (q *Queries) DefaultPrivacyPolicy(ctx context.Context, shouldTriggerBulk bo return policy, err } -func preparePrivacyPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*PrivacyPolicy, error)) { +func preparePrivacyPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*PrivacyPolicy, error)) { return sq.Select( PrivacyColID.identifier(), PrivacyColSequence.identifier(), @@ -189,7 +188,7 @@ func preparePrivacyPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sele PrivacyColIsDefault.identifier(), PrivacyColState.identifier(), ). - From(privacyTable.identifier() + db.Timetravel(call.Took(ctx))). + From(privacyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*PrivacyPolicy, error) { policy := new(PrivacyPolicy) diff --git a/internal/query/privacy_policy_test.go b/internal/query/privacy_policy_test.go index ade541d0cc..1777ca1991 100644 --- a/internal/query/privacy_policy_test.go +++ b/internal/query/privacy_policy_test.go @@ -27,8 +27,7 @@ var ( ` projections.privacy_policies4.custom_link_text,` + ` projections.privacy_policies4.is_default,` + ` projections.privacy_policies4.state` + - ` FROM projections.privacy_policies4` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.privacy_policies4` preparePrivacyPolicyCols = []string{ "id", "sequence", @@ -138,7 +137,7 @@ func Test_PrivacyPolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/project.go b/internal/query/project.go index a92448f25d..7501047182 100644 --- a/internal/query/project.go +++ b/internal/query/project.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -109,7 +108,7 @@ func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id st traceSpan.EndWithError(err) } - stmt, scan := prepareProjectQuery(ctx, q.client) + stmt, scan := prepareProjectQuery() eq := sq.Eq{ ProjectColumnID.identifier(): id, ProjectColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -130,7 +129,7 @@ func (q *Queries) SearchProjects(ctx context.Context, queries *ProjectSearchQuer ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareProjectsQuery(ctx, q.client) + query, scan := prepareProjectsQuery() eq := sq.Eq{ProjectColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -193,7 +192,7 @@ func (q *ProjectSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder return query } -func prepareProjectQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { +func prepareProjectQuery() (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { return sq.Select( ProjectColumnID.identifier(), ProjectColumnCreationDate.identifier(), @@ -206,7 +205,7 @@ func prepareProjectQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil ProjectColumnProjectRoleCheck.identifier(), ProjectColumnHasProjectCheck.identifier(), ProjectColumnPrivateLabelingSetting.identifier()). - From(projectsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(projectsTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Project, error) { p := new(Project) @@ -233,7 +232,7 @@ func prepareProjectQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } } -func prepareProjectsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Projects, error)) { +func prepareProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Projects, error)) { return sq.Select( ProjectColumnID.identifier(), ProjectColumnCreationDate.identifier(), @@ -247,7 +246,7 @@ func prepareProjectsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui ProjectColumnHasProjectCheck.identifier(), ProjectColumnPrivateLabelingSetting.identifier(), countColumn.identifier()). - From(projectsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(projectsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Projects, error) { projects := make([]*Project, 0) diff --git a/internal/query/project_grant.go b/internal/query/project_grant.go index 1bc68e984a..b971593c77 100644 --- a/internal/query/project_grant.go +++ b/internal/query/project_grant.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -116,7 +115,7 @@ func (q *Queries) ProjectGrantByID(ctx context.Context, shouldTriggerBulk bool, traceSpan.EndWithError(err) } - stmt, scan := prepareProjectGrantQuery(ctx, q.client) + stmt, scan := prepareProjectGrantQuery() eq := sq.Eq{ ProjectGrantColumnGrantID.identifier(): id, ProjectGrantColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -137,7 +136,7 @@ func (q *Queries) ProjectGrantByIDAndGrantedOrg(ctx context.Context, id, granted ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareProjectGrantQuery(ctx, q.client) + stmt, scan := prepareProjectGrantQuery() eq := sq.Eq{ ProjectGrantColumnGrantID.identifier(): id, ProjectGrantColumnGrantedOrgID.identifier(): grantedOrg, @@ -159,7 +158,7 @@ func (q *Queries) SearchProjectGrants(ctx context.Context, queries *ProjectGrant ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareProjectGrantsQuery(ctx, q.client) + query, scan := prepareProjectGrantsQuery() eq := sq.Eq{ ProjectGrantColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -264,7 +263,7 @@ func (q *ProjectGrantSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBui return query } -func prepareProjectGrantQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*ProjectGrant, error)) { +func prepareProjectGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*ProjectGrant, error)) { resourceOwnerOrgTable := orgsTable.setAlias(ProjectGrantResourceOwnerTableAlias) resourceOwnerIDColumn := OrgColumnID.setTable(resourceOwnerOrgTable) grantedOrgTable := orgsTable.setAlias(ProjectGrantGrantedOrgTableAlias) @@ -286,7 +285,7 @@ func prepareProjectGrantQuery(ctx context.Context, db prepareDatabase) (sq.Selec PlaceholderFormat(sq.Dollar). LeftJoin(join(ProjectColumnID, ProjectGrantColumnProjectID)). LeftJoin(join(resourceOwnerIDColumn, ProjectGrantColumnResourceOwner)). - LeftJoin(join(grantedOrgIDColumn, ProjectGrantColumnGrantedOrgID) + db.Timetravel(call.Took(ctx))), + LeftJoin(join(grantedOrgIDColumn, ProjectGrantColumnGrantedOrgID)), func(row *sql.Row) (*ProjectGrant, error) { grant := new(ProjectGrant) var ( @@ -323,7 +322,7 @@ func prepareProjectGrantQuery(ctx context.Context, db prepareDatabase) (sq.Selec } } -func prepareProjectGrantsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*ProjectGrants, error)) { +func prepareProjectGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*ProjectGrants, error)) { resourceOwnerOrgTable := orgsTable.setAlias(ProjectGrantResourceOwnerTableAlias) resourceOwnerIDColumn := OrgColumnID.setTable(resourceOwnerOrgTable) grantedOrgTable := orgsTable.setAlias(ProjectGrantGrantedOrgTableAlias) @@ -346,7 +345,7 @@ func prepareProjectGrantsQuery(ctx context.Context, db prepareDatabase) (sq.Sele PlaceholderFormat(sq.Dollar). LeftJoin(join(ProjectColumnID, ProjectGrantColumnProjectID)). LeftJoin(join(resourceOwnerIDColumn, ProjectGrantColumnResourceOwner)). - LeftJoin(join(grantedOrgIDColumn, ProjectGrantColumnGrantedOrgID) + db.Timetravel(call.Took(ctx))), + LeftJoin(join(grantedOrgIDColumn, ProjectGrantColumnGrantedOrgID)), func(rows *sql.Rows) (*ProjectGrants, error) { projects := make([]*ProjectGrant, 0) var ( diff --git a/internal/query/project_grant_member.go b/internal/query/project_grant_member.go index 0820ada826..a9cc49c498 100644 --- a/internal/query/project_grant_member.go +++ b/internal/query/project_grant_member.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/zerrors" @@ -82,7 +81,7 @@ func (q *ProjectGrantMembersQuery) toQuery(query sq.SelectBuilder) sq.SelectBuil } func (q *Queries) ProjectGrantMembers(ctx context.Context, queries *ProjectGrantMembersQuery) (members *Members, err error) { - query, scan := prepareProjectGrantMembersQuery(ctx, q.client) + query, scan := prepareProjectGrantMembersQuery() eq := sq.Eq{ProjectGrantMemberInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -106,7 +105,7 @@ func (q *Queries) ProjectGrantMembers(ctx context.Context, queries *ProjectGrant return members, err } -func prepareProjectGrantMembersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { +func prepareProjectGrantMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { return sq.Select( ProjectGrantMemberCreationDate.identifier(), ProjectGrantMemberChangeDate.identifier(), @@ -129,7 +128,7 @@ func prepareProjectGrantMembersQuery(ctx context.Context, db prepareDatabase) (s LeftJoin(join(MachineUserIDCol, ProjectGrantMemberUserID)). LeftJoin(join(UserIDCol, ProjectGrantMemberUserID)). LeftJoin(join(LoginNameUserIDCol, ProjectGrantMemberUserID)). - LeftJoin(join(ProjectGrantColumnGrantID, ProjectGrantMemberGrantID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(ProjectGrantColumnGrantID, ProjectGrantMemberGrantID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/project_grant_member_test.go b/internal/query/project_grant_member_test.go index 72eaf76d6e..23d1258b7c 100644 --- a/internal/query/project_grant_member_test.go +++ b/internal/query/project_grant_member_test.go @@ -46,7 +46,6 @@ var ( "LEFT JOIN projections.project_grants4 " + "ON members.grant_id = projections.project_grants4.grant_id " + "AND members.instance_id = projections.project_grants4.instance_id " + - `AS OF SYSTEM TIME '-1 ms' ` + "WHERE projections.login_names3.is_primary = $1") projectGrantMembersColumns = []string{ "creation_date", @@ -302,7 +301,7 @@ func Test_ProjectGrantMemberPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/project_grant_test.go b/internal/query/project_grant_test.go index 6d2131dfc4..2801e0b23e 100644 --- a/internal/query/project_grant_test.go +++ b/internal/query/project_grant_test.go @@ -30,8 +30,7 @@ var ( ` FROM projections.project_grants4 ` + ` LEFT JOIN projections.projects4 ON projections.project_grants4.project_id = projections.projects4.id AND projections.project_grants4.instance_id = projections.projects4.instance_id ` + ` LEFT JOIN projections.orgs1 AS r ON projections.project_grants4.resource_owner = r.id AND projections.project_grants4.instance_id = r.instance_id` + - ` LEFT JOIN projections.orgs1 AS o ON projections.project_grants4.granted_org_id = o.id AND projections.project_grants4.instance_id = o.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.orgs1 AS o ON projections.project_grants4.granted_org_id = o.id AND projections.project_grants4.instance_id = o.instance_id` projectGrantsCols = []string{ "project_id", "grant_id", @@ -62,8 +61,7 @@ var ( ` FROM projections.project_grants4 ` + ` LEFT JOIN projections.projects4 ON projections.project_grants4.project_id = projections.projects4.id AND projections.project_grants4.instance_id = projections.projects4.instance_id ` + ` LEFT JOIN projections.orgs1 AS r ON projections.project_grants4.resource_owner = r.id AND projections.project_grants4.instance_id = r.instance_id` + - ` LEFT JOIN projections.orgs1 AS o ON projections.project_grants4.granted_org_id = o.id AND projections.project_grants4.instance_id = o.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.orgs1 AS o ON projections.project_grants4.granted_org_id = o.id AND projections.project_grants4.instance_id = o.instance_id` projectGrantCols = []string{ "project_id", "grant_id", @@ -573,7 +571,7 @@ func Test_ProjectGrantPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/project_member.go b/internal/query/project_member.go index 347eac12b9..1b66b45ccc 100644 --- a/internal/query/project_member.go +++ b/internal/query/project_member.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -73,7 +72,7 @@ func (q *Queries) ProjectMembers(ctx context.Context, queries *ProjectMembersQue ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareProjectMembersQuery(ctx, q.client) + query, scan := prepareProjectMembersQuery() eq := sq.Eq{ProjectMemberInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -97,7 +96,7 @@ func (q *Queries) ProjectMembers(ctx context.Context, queries *ProjectMembersQue return members, err } -func prepareProjectMembersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { +func prepareProjectMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { return sq.Select( ProjectMemberCreationDate.identifier(), ProjectMemberChangeDate.identifier(), @@ -119,7 +118,7 @@ func prepareProjectMembersQuery(ctx context.Context, db prepareDatabase) (sq.Sel LeftJoin(join(HumanUserIDCol, ProjectMemberUserID)). LeftJoin(join(MachineUserIDCol, ProjectMemberUserID)). LeftJoin(join(UserIDCol, ProjectMemberUserID)). - LeftJoin(join(LoginNameUserIDCol, ProjectMemberUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, ProjectMemberUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/project_member_test.go b/internal/query/project_member_test.go index 24548dadfb..6e552eb2ec 100644 --- a/internal/query/project_member_test.go +++ b/internal/query/project_member_test.go @@ -43,7 +43,6 @@ var ( "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id " + "AND members.instance_id = projections.login_names3.instance_id " + - `AS OF SYSTEM TIME '-1 ms' ` + "WHERE projections.login_names3.is_primary = $1") projectMembersColumns = []string{ "creation_date", @@ -299,7 +298,7 @@ func Test_ProjectMemberPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/project_role.go b/internal/query/project_role.go index 76e113da65..ab4f40ca38 100644 --- a/internal/query/project_role.go +++ b/internal/query/project_role.go @@ -9,7 +9,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -94,7 +93,7 @@ func (q *Queries) SearchProjectRoles(ctx context.Context, shouldTriggerBulk bool eq := sq.Eq{ProjectRoleColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} - query, scan := prepareProjectRolesQuery(ctx, q.client) + query, scan := prepareProjectRolesQuery() stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-3N9ff", "Errors.Query.InvalidRequest") @@ -126,7 +125,7 @@ func (q *Queries) SearchGrantedProjectRoles(ctx context.Context, grantID, grante eq := sq.Eq{ProjectRoleColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} - query, scan := prepareProjectRolesQuery(ctx, q.client) + query, scan := prepareProjectRolesQuery() stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-3N9ff", "Errors.Query.InvalidRequest") @@ -207,7 +206,7 @@ func (q *ProjectRoleSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuil return query } -func prepareProjectRolesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*ProjectRoles, error)) { +func prepareProjectRolesQuery() (sq.SelectBuilder, func(*sql.Rows) (*ProjectRoles, error)) { return sq.Select( ProjectRoleColumnProjectID.identifier(), ProjectRoleColumnCreationDate.identifier(), @@ -218,7 +217,7 @@ func prepareProjectRolesQuery(ctx context.Context, db prepareDatabase) (sq.Selec ProjectRoleColumnDisplayName.identifier(), ProjectRoleColumnGroupName.identifier(), countColumn.identifier()). - From(projectRolesTable.identifier() + db.Timetravel(call.Took(ctx))). + From(projectRolesTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*ProjectRoles, error) { projects := make([]*ProjectRole, 0) diff --git a/internal/query/project_role_test.go b/internal/query/project_role_test.go index 516a4df169..468aafaa19 100644 --- a/internal/query/project_role_test.go +++ b/internal/query/project_role_test.go @@ -19,8 +19,7 @@ var ( ` projections.project_roles4.display_name,` + ` projections.project_roles4.group_name,` + ` COUNT(*) OVER ()` + - ` FROM projections.project_roles4` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.project_roles4` prepareProjectRolesCols = []string{ "project_id", "creation_date", @@ -175,7 +174,7 @@ func Test_ProjectRolePrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/project_test.go b/internal/query/project_test.go index a621c27f42..1eafcb69a8 100644 --- a/internal/query/project_test.go +++ b/internal/query/project_test.go @@ -39,8 +39,7 @@ var ( ` projections.projects4.has_project_check,` + ` projections.projects4.private_labeling_setting,` + ` COUNT(*) OVER ()` + - ` FROM projections.projects4` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.projects4` prepareProjectsCols = []string{ "id", "creation_date", @@ -67,8 +66,7 @@ var ( ` projections.projects4.project_role_check,` + ` projections.projects4.has_project_check,` + ` projections.projects4.private_labeling_setting` + - ` FROM projections.projects4` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.projects4` prepareProjectCols = []string{ "id", "creation_date", @@ -314,7 +312,7 @@ func Test_ProjectPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/projection/eventstore_field.go b/internal/query/projection/eventstore_field.go index 5dbdad717a..73e3ac2c82 100644 --- a/internal/query/projection/eventstore_field.go +++ b/internal/query/projection/eventstore_field.go @@ -5,6 +5,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/permission" "github.com/zitadel/zitadel/internal/repository/project" ) @@ -13,6 +14,7 @@ const ( fieldsOrgDomainVerified = "org_domain_verified_fields" fieldsInstanceDomain = "instance_domain_fields" fieldsMemberships = "membership_fields" + fieldsPermission = "permission_fields" ) func newFillProjectGrantFields(config handler.Config) *handler.FieldHandler { @@ -83,3 +85,16 @@ func newFillMembershipFields(config handler.Config) *handler.FieldHandler { }, ) } + +func newFillPermissionFields(config handler.Config) *handler.FieldHandler { + return handler.NewFieldHandler( + &config, + permission.PermissionSearchField, + map[eventstore.AggregateType][]eventstore.EventType{ + permission.AggregateType: { + permission.AddedType, + permission.RemovedType, + }, + }, + ) +} diff --git a/internal/query/projection/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/instance_features.go b/internal/query/projection/instance_features.go index 798af6693c..34100a0d66 100644 --- a/internal/query/projection/instance_features.go +++ b/internal/query/projection/instance_features.go @@ -80,10 +80,6 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.InstanceTokenExchangeEventType, Reduce: reduceInstanceSetFeature[bool], }, - { - Event: feature_v2.InstanceActionsEventType, - Reduce: reduceInstanceSetFeature[bool], - }, { Event: feature_v2.InstanceImprovedPerformanceEventType, Reduce: reduceInstanceSetFeature[[]feature.ImprovedPerformanceType], diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index d6647d0961..07953a27e8 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -2,6 +2,9 @@ package projection import ( "context" + "fmt" + + "github.com/zitadel/logging" internal_authz "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" @@ -86,9 +89,11 @@ var ( OrgDomainVerifiedFields *handler.FieldHandler InstanceDomainFields *handler.FieldHandler MembershipFields *handler.FieldHandler + PermissionFields *handler.FieldHandler ) 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) @@ -97,6 +102,7 @@ type projection interface { var ( projections []projection + fields []*handler.FieldHandler ) func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, config Config, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, certEncryptionAlgorithm crypto.EncryptionAlgorithm, systemUsers map[string]*internal_authz.SystemAPIUser) error { @@ -176,8 +182,11 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, OrgDomainVerifiedFields = newFillOrgDomainVerifiedFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsOrgDomainVerified])) InstanceDomainFields = newFillInstanceDomainFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsInstanceDomain])) MembershipFields = newFillMembershipFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsMemberships])) + PermissionFields = newFillPermissionFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsPermission])) + // Don't forget to add the new field handler to [ProjectInstanceFields] newProjectionsList() + newFieldsList() return nil } @@ -201,11 +210,25 @@ func Start(ctx context.Context) { } func ProjectInstance(ctx context.Context) error { - for _, projection := range projections { + 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") _, err := projection.Trigger(ctx) if err != nil { return err } + logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).Info("projection done") + } + return nil +} + +func ProjectInstanceFields(ctx context.Context) error { + 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") + err := fieldProjection.Trigger(ctx) + if err != nil { + return err + } + logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).Info("fields projection done") } return nil } @@ -234,6 +257,16 @@ func applyCustomConfig(config handler.Config, customConfig CustomConfig) handler return config } +func newFieldsList() { + fields = []*handler.FieldHandler{ + ProjectGrantFields, + OrgDomainVerifiedFields, + InstanceDomainFields, + MembershipFields, + PermissionFields, + } +} + // we know this is ugly, but we need to have a singleton slice of all projections // and are only able to initialize it after all projections are created // as setup and start currently create them individually, we make sure we get the right one diff --git a/internal/query/projection/system_features.go b/internal/query/projection/system_features.go index f6f0a36d56..de54054e78 100644 --- a/internal/query/projection/system_features.go +++ b/internal/query/projection/system_features.go @@ -72,10 +72,6 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.SystemTokenExchangeEventType, Reduce: reduceSystemSetFeature[bool], }, - { - Event: feature_v2.SystemActionsEventType, - Reduce: reduceSystemSetFeature[bool], - }, { Event: feature_v2.SystemImprovedPerformanceEventType, Reduce: reduceSystemSetFeature[[]feature.ImprovedPerformanceType], diff --git a/internal/query/query.go b/internal/query/query.go index 0a90e9e4f9..c0c051f7b7 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -112,10 +112,6 @@ func (q *Queries) Health(ctx context.Context) error { return q.client.Ping() } -type prepareDatabase interface { - Timetravel(d time.Duration) string -} - // cleanStaticQueries removes whitespaces, // such as ` `, \t, \n, from queries to improve // readability in logs and errors. diff --git a/internal/query/quota.go b/internal/query/quota.go index 50bc28cabc..77d2f3892b 100644 --- a/internal/query/quota.go +++ b/internal/query/quota.go @@ -62,7 +62,7 @@ type Quota struct { func (q *Queries) GetQuota(ctx context.Context, instanceID string, unit quota.Unit) (qu *Quota, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareQuotaQuery(ctx, q.client) + query, scan := prepareQuotaQuery() stmt, args, err := query.Where( sq.Eq{ QuotaColumnInstanceID.identifier(): instanceID, @@ -79,7 +79,7 @@ func (q *Queries) GetQuota(ctx context.Context, instanceID string, unit quota.Un return qu, err } -func prepareQuotaQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Quota, error)) { +func prepareQuotaQuery() (sq.SelectBuilder, func(*sql.Row) (*Quota, error)) { return sq. Select( QuotaColumnID.identifier(), diff --git a/internal/query/quota_notifications.go b/internal/query/quota_notifications.go index 0015278b20..9e3cb1a10c 100644 --- a/internal/query/quota_notifications.go +++ b/internal/query/quota_notifications.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/quota" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -59,7 +58,7 @@ func (q *Queries) GetDueQuotaNotifications(ctx context.Context, instanceID strin ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() usedRel := uint16(math.Floor(float64(usedAbs*100) / float64(qu.Amount))) - query, scan := prepareQuotaNotificationsQuery(ctx, q.client) + query, scan := prepareQuotaNotificationsQuery() stmt, args, err := query.Where( sq.And{ sq.Eq{ @@ -149,7 +148,7 @@ func calculateThreshold(usedRel, notificationPercent uint16) uint16 { return uint16(times+percent-1)*100 + notificationPercent } -func prepareQuotaNotificationsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*QuotaNotifications, error)) { +func prepareQuotaNotificationsQuery() (sq.SelectBuilder, func(*sql.Rows) (*QuotaNotifications, error)) { return sq.Select( QuotaNotificationColumnID.identifier(), QuotaNotificationColumnCallURL.identifier(), @@ -157,7 +156,7 @@ func prepareQuotaNotificationsQuery(ctx context.Context, db prepareDatabase) (sq QuotaNotificationColumnRepeat.identifier(), QuotaNotificationColumnNextDueThreshold.identifier(), ). - From(quotaNotificationsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(quotaNotificationsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*QuotaNotifications, error) { cfgs := &QuotaNotifications{Configs: []*QuotaNotification{}} for rows.Next() { diff --git a/internal/query/quota_notifications_test.go b/internal/query/quota_notifications_test.go index a86b31df57..5515d2b3a0 100644 --- a/internal/query/quota_notifications_test.go +++ b/internal/query/quota_notifications_test.go @@ -92,8 +92,7 @@ var ( ` projections.quotas_notifications.percent,` + ` projections.quotas_notifications.repeat,` + ` projections.quotas_notifications.next_due_threshold` + - ` FROM projections.quotas_notifications` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` FROM projections.quotas_notifications`) quotaNotificationsCols = []string{ "id", @@ -175,7 +174,7 @@ func Test_prepareQuotaNotificationsQuery(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/quota_periods.go b/internal/query/quota_periods.go index 6ec42deba3..c954108c5f 100644 --- a/internal/query/quota_periods.go +++ b/internal/query/quota_periods.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/quota" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -40,7 +39,7 @@ var ( func (q *Queries) GetRemainingQuotaUsage(ctx context.Context, instanceID string, unit quota.Unit) (remaining *uint64, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareRemainingQuotaUsageQuery(ctx, q.client) + stmt, scan := prepareRemainingQuotaUsageQuery() query, args, err := stmt.Where( sq.And{ sq.Eq{ @@ -66,13 +65,13 @@ func (q *Queries) GetRemainingQuotaUsage(ctx context.Context, instanceID string, return remaining, err } -func prepareRemainingQuotaUsageQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*uint64, error)) { +func prepareRemainingQuotaUsageQuery() (sq.SelectBuilder, func(*sql.Row) (*uint64, error)) { return sq. Select( "greatest(0, " + QuotaColumnAmount.identifier() + "-" + QuotaPeriodColumnUsage.identifier() + ")", ). From(quotaPeriodsTable.identifier()). - Join(join(QuotaColumnUnit, QuotaPeriodColumnUnit) + db.Timetravel(call.Took(ctx))). + Join(join(QuotaColumnUnit, QuotaPeriodColumnUnit)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*uint64, error) { remaining := new(uint64) err := row.Scan(remaining) diff --git a/internal/query/quota_periods_test.go b/internal/query/quota_periods_test.go index 0f44c5e547..385a49c557 100644 --- a/internal/query/quota_periods_test.go +++ b/internal/query/quota_periods_test.go @@ -14,8 +14,7 @@ import ( var ( expectedRemainingQuotaUsageQuery = regexp.QuoteMeta(`SELECT greatest(0, projections.quotas.amount-projections.quotas_periods.usage)` + ` FROM projections.quotas_periods` + - ` JOIN projections.quotas ON projections.quotas_periods.unit = projections.quotas.unit AND projections.quotas_periods.instance_id = projections.quotas.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` JOIN projections.quotas ON projections.quotas_periods.unit = projections.quotas.unit AND projections.quotas_periods.instance_id = projections.quotas.instance_id`) remainingQuotaUsageCols = []string{ "usage", } @@ -84,7 +83,7 @@ func Test_prepareRemainingQuotaUsageQuery(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/quota_test.go b/internal/query/quota_test.go index a92938e0cb..1e3ff1e9b2 100644 --- a/internal/query/quota_test.go +++ b/internal/query/quota_test.go @@ -110,7 +110,7 @@ func Test_QuotaPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/restrictions.go b/internal/query/restrictions.go index 9e0dd37aa6..8cff5737f7 100644 --- a/internal/query/restrictions.go +++ b/internal/query/restrictions.go @@ -10,7 +10,6 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" @@ -72,7 +71,7 @@ func (q *Queries) GetInstanceRestrictions(ctx context.Context) (restrictions Res ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareRestrictionsQuery(ctx, q.client) + stmt, scan := prepareRestrictionsQuery() instanceID := authz.GetInstance(ctx).InstanceID() query, args, err := stmt.Where(sq.Eq{ RestrictionsColumnInstanceID.identifier(): instanceID, @@ -92,7 +91,7 @@ func (q *Queries) GetInstanceRestrictions(ctx context.Context) (restrictions Res return restrictions, err } -func prepareRestrictionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (Restrictions, error)) { +func prepareRestrictionsQuery() (sq.SelectBuilder, func(*sql.Row) (Restrictions, error)) { return sq.Select( RestrictionsColumnAggregateID.identifier(), RestrictionsColumnCreationDate.identifier(), @@ -102,7 +101,7 @@ func prepareRestrictionsQuery(ctx context.Context, db prepareDatabase) (sq.Selec RestrictionsColumnDisallowPublicOrgRegistration.identifier(), RestrictionsColumnAllowedLanguages.identifier(), ). - From(restrictionsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(restrictionsTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (restrictions Restrictions, err error) { allowedLanguages := database.TextArray[string](make([]string, 0)) diff --git a/internal/query/restrictions_test.go b/internal/query/restrictions_test.go index cc7ee8442a..69ed81ef6d 100644 --- a/internal/query/restrictions_test.go +++ b/internal/query/restrictions_test.go @@ -21,8 +21,7 @@ var ( " projections.restrictions2.sequence," + " projections.restrictions2.disallow_public_org_registration," + " projections.restrictions2.allowed_languages" + - " FROM projections.restrictions2" + - " AS OF SYSTEM TIME '-1 ms'", + " FROM projections.restrictions2", ) restrictionsCols = []string{ @@ -115,7 +114,7 @@ func Test_RestrictionsPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.want.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.want.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/saml_request.go b/internal/query/saml_request.go index a0f6fdc6cd..784627bc59 100644 --- a/internal/query/saml_request.go +++ b/internal/query/saml_request.go @@ -5,13 +5,12 @@ import ( "database/sql" _ "embed" "errors" - "fmt" "time" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -28,9 +27,9 @@ type SamlRequest struct { Binding string } -func (a *SamlRequest) checkLoginClient(ctx context.Context) error { +func (a *SamlRequest) checkLoginClient(ctx context.Context, permissionCheck domain.PermissionCheck) error { if uid := authz.GetCtxData(ctx).UserID; uid != a.LoginClient { - return zerrors.ThrowPermissionDenied(nil, "OIDCv2-aL0ag", "Errors.SamlRequest.WrongLoginClient") + return permissionCheck(ctx, domain.PermissionSessionRead, authz.GetInstance(ctx).InstanceID(), "") } return nil } @@ -38,10 +37,6 @@ func (a *SamlRequest) checkLoginClient(ctx context.Context) error { //go:embed saml_request_by_id.sql var samlRequestByIDQuery string -func (q *Queries) samlRequestByIDQuery(ctx context.Context) string { - return fmt.Sprintf(samlRequestByIDQuery, q.client.Timetravel(call.Took(ctx))) -} - func (q *Queries) SamlRequestByID(ctx context.Context, shouldTriggerBulk bool, id string, checkLoginClient bool) (_ *SamlRequest, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -61,7 +56,7 @@ func (q *Queries) SamlRequestByID(ctx context.Context, shouldTriggerBulk bool, i &dst.ID, &dst.CreationDate, &dst.LoginClient, &dst.Issuer, &dst.ACS, &dst.RelayState, &dst.Binding, ) }, - q.samlRequestByIDQuery(ctx), + samlRequestByIDQuery, id, authz.GetInstance(ctx).InstanceID(), ) if errors.Is(err, sql.ErrNoRows) { @@ -72,7 +67,7 @@ func (q *Queries) SamlRequestByID(ctx context.Context, shouldTriggerBulk bool, i } if checkLoginClient { - if err = dst.checkLoginClient(ctx); err != nil { + if err = dst.checkLoginClient(ctx, q.checkPermission); err != nil { return nil, err } } diff --git a/internal/query/saml_request_by_id.sql b/internal/query/saml_request_by_id.sql index ac1c60058f..73eadb01ad 100644 --- a/internal/query/saml_request_by_id.sql +++ b/internal/query/saml_request_by_id.sql @@ -6,6 +6,6 @@ select acs, relay_state, binding -from projections.saml_requests %s +from projections.saml_requests where id = $1 and instance_id = $2 limit 1; diff --git a/internal/query/saml_request_test.go b/internal/query/saml_request_test.go index 5cf58369cb..3a062ac5fd 100644 --- a/internal/query/saml_request_test.go +++ b/internal/query/saml_request_test.go @@ -1,6 +1,7 @@ package query import ( + "context" "database/sql" "database/sql/driver" _ "embed" @@ -13,6 +14,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -20,7 +22,6 @@ import ( func TestQueries_SamlRequestByID(t *testing.T) { expQuery := regexp.QuoteMeta(fmt.Sprintf( samlRequestByIDQuery, - asOfSystemTime, )) cols := []string{ @@ -38,11 +39,12 @@ func TestQueries_SamlRequestByID(t *testing.T) { checkLoginClient bool } tests := []struct { - name string - args args - expect sqlExpectation - want *SamlRequest - wantErr error + name string + args args + expect sqlExpectation + permissionCheck domain.PermissionCheck + want *SamlRequest + wantErr error }{ { name: "success, all values", @@ -89,7 +91,7 @@ func TestQueries_SamlRequestByID(t *testing.T) { wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Ou8ue", "Errors.Internal"), }, { - name: "wrong login client", + name: "wrong login client/ not permitted", args: args{ shouldTriggerBulk: false, id: "123", @@ -104,16 +106,48 @@ func TestQueries_SamlRequestByID(t *testing.T) { "relayState", "binding", }, "123", "instanceID"), - wantErr: zerrors.ThrowPermissionDeniedf(nil, "OIDCv2-aL0ag", "Errors.SamlRequest.WrongLoginClient"), + permissionCheck: func(ctx context.Context, permission, orgID, resourceID string) (err error) { + return zerrors.ThrowPermissionDenied(nil, "id", "not permitted") + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "id", "not permitted"), + }, + { + name: "wrong login client / permitted", + args: args{ + shouldTriggerBulk: false, + id: "123", + checkLoginClient: true, + }, + expect: mockQuery(expQuery, cols, []driver.Value{ + "id", + testNow, + "otherLoginClient", + "issuer", + "acs", + "relayState", + "binding", + }, "123", "instanceID"), + permissionCheck: func(ctx context.Context, permission, orgID, resourceID string) (err error) { + return nil + }, + want: &SamlRequest{ + ID: "id", + CreationDate: testNow, + LoginClient: "otherLoginClient", + Issuer: "issuer", + ACS: "acs", + RelayState: "relayState", + Binding: "binding", + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { execMock(t, tt.expect, func(db *sql.DB) { q := &Queries{ + checkPermission: tt.permissionCheck, client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") diff --git a/internal/query/saml_sp_test.go b/internal/query/saml_sp_test.go index 4aafd95de1..35bf93c5fe 100644 --- a/internal/query/saml_sp_test.go +++ b/internal/query/saml_sp_test.go @@ -109,8 +109,7 @@ func TestQueries_ActiveSAMLServiceProviderByID(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") diff --git a/internal/query/secret_generator_test.go b/internal/query/secret_generator_test.go index 683dc3441e..9ce8e71769 100644 --- a/internal/query/secret_generator_test.go +++ b/internal/query/secret_generator_test.go @@ -26,8 +26,7 @@ var ( ` projections.secret_generators2.include_upper_letters,` + ` projections.secret_generators2.include_digits,` + ` projections.secret_generators2.include_symbols` + - ` FROM projections.secret_generators2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.secret_generators2` prepareSecretGeneratorCols = []string{ "aggregate_id", "generator_type", @@ -55,8 +54,7 @@ var ( ` projections.secret_generators2.include_digits,` + ` projections.secret_generators2.include_symbols,` + ` COUNT(*) OVER ()` + - ` FROM projections.secret_generators2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.secret_generators2` prepareSecretGeneratorsCols = []string{ "aggregate_id", "generator_type", @@ -312,7 +310,7 @@ func Test_SecretGeneratorsPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/secret_generators.go b/internal/query/secret_generators.go index 8ee8694d2b..c267d7b290 100644 --- a/internal/query/secret_generators.go +++ b/internal/query/secret_generators.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" @@ -127,7 +126,7 @@ func (q *Queries) SecretGeneratorByType(ctx context.Context, generatorType domai defer func() { span.EndWithError(err) }() instanceID := authz.GetInstance(ctx).InstanceID() - stmt, scan := prepareSecretGeneratorQuery(ctx, q.client) + stmt, scan := prepareSecretGeneratorQuery() query, args, err := stmt.Where(sq.Eq{ SecretGeneratorColumnGeneratorType.identifier(): generatorType, SecretGeneratorColumnInstanceID.identifier(): instanceID, @@ -148,7 +147,7 @@ func (q *Queries) SearchSecretGenerators(ctx context.Context, queries *SecretGen ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareSecretGeneratorsQuery(ctx, q.client) + query, scan := prepareSecretGeneratorsQuery() stmt, args, err := queries.toQuery(query). Where(sq.Eq{ SecretGeneratorColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -180,7 +179,7 @@ func NewSecretGeneratorTypeSearchQuery(value int32) (SearchQuery, error) { return NewNumberQuery(SecretGeneratorColumnGeneratorType, value, NumberEquals) } -func prepareSecretGeneratorQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*SecretGenerator, error)) { +func prepareSecretGeneratorQuery() (sq.SelectBuilder, func(*sql.Row) (*SecretGenerator, error)) { return sq.Select( SecretGeneratorColumnAggregateID.identifier(), SecretGeneratorColumnGeneratorType.identifier(), @@ -194,7 +193,7 @@ func prepareSecretGeneratorQuery(ctx context.Context, db prepareDatabase) (sq.Se SecretGeneratorColumnIncludeUpperLetters.identifier(), SecretGeneratorColumnIncludeDigits.identifier(), SecretGeneratorColumnIncludeSymbols.identifier()). - From(secretGeneratorsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(secretGeneratorsTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SecretGenerator, error) { secretGenerator := new(SecretGenerator) @@ -222,7 +221,7 @@ func prepareSecretGeneratorQuery(ctx context.Context, db prepareDatabase) (sq.Se } } -func prepareSecretGeneratorsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*SecretGenerators, error)) { +func prepareSecretGeneratorsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SecretGenerators, error)) { return sq.Select( SecretGeneratorColumnAggregateID.identifier(), SecretGeneratorColumnGeneratorType.identifier(), @@ -237,7 +236,7 @@ func prepareSecretGeneratorsQuery(ctx context.Context, db prepareDatabase) (sq.S SecretGeneratorColumnIncludeDigits.identifier(), SecretGeneratorColumnIncludeSymbols.identifier(), countColumn.identifier()). - From(secretGeneratorsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(secretGeneratorsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*SecretGenerators, error) { secretGenerators := make([]*SecretGenerator, 0) diff --git a/internal/query/security_policy.go b/internal/query/security_policy.go index 51938abdae..7a3fb3fa89 100644 --- a/internal/query/security_policy.go +++ b/internal/query/security_policy.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/zerrors" @@ -63,7 +62,7 @@ type SecurityPolicy struct { } func (q *Queries) SecurityPolicy(ctx context.Context) (policy *SecurityPolicy, err error) { - stmt, scan := prepareSecurityPolicyQuery(ctx, q.client) + stmt, scan := prepareSecurityPolicyQuery() query, args, err := stmt.Where(sq.Eq{ SecurityPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() @@ -78,7 +77,7 @@ func (q *Queries) SecurityPolicy(ctx context.Context) (policy *SecurityPolicy, e return policy, err } -func prepareSecurityPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*SecurityPolicy, error)) { +func prepareSecurityPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*SecurityPolicy, error)) { return sq.Select( SecurityPolicyColumnInstanceID.identifier(), SecurityPolicyColumnCreationDate.identifier(), @@ -88,7 +87,7 @@ func prepareSecurityPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sel SecurityPolicyColumnEnableIframeEmbedding.identifier(), SecurityPolicyColumnAllowedOrigins.identifier(), SecurityPolicyColumnEnableImpersonation.identifier()). - From(securityPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + From(securityPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SecurityPolicy, error) { securityPolicy := new(SecurityPolicy) diff --git a/internal/query/session.go b/internal/query/session.go index 706465949e..111eb462a0 100644 --- a/internal/query/session.go +++ b/internal/query/session.go @@ -13,7 +13,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -261,7 +260,7 @@ func (q *Queries) sessionByID(ctx context.Context, shouldTriggerBulk bool, id st traceSpan.EndWithError(err) } - query, scan := prepareSessionQuery(ctx, q.client) + query, scan := prepareSessionQuery() stmt, args, err := query.Where( sq.Eq{ SessionColumnID.identifier(): id, @@ -297,7 +296,7 @@ func (q *Queries) searchSessions(ctx context.Context, queries *SessionsSearchQue ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareSessionsQuery(ctx, q.client) + query, scan := prepareSessionsQuery() stmt, args, err := queries.toQuery(query). Where(sq.Eq{ SessionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -343,7 +342,7 @@ func NewCreationDateQuery(datetime time.Time, compare TimestampComparison) (Sear return NewTimestampQuery(SessionColumnCreationDate, datetime, compare) } -func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Session, string, error)) { +func prepareSessionQuery() (sq.SelectBuilder, func(*sql.Row) (*Session, string, error)) { return sq.Select( SessionColumnID.identifier(), SessionColumnCreationDate.identifier(), @@ -374,7 +373,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil ).From(sessionsTable.identifier()). LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)). LeftJoin(join(HumanUserIDCol, SessionColumnUserID)). - LeftJoin(join(UserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(UserIDCol, SessionColumnUserID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Session, string, error) { session := new(Session) @@ -456,7 +455,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } } -func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Sessions, error)) { +func prepareSessionsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Sessions, error)) { return sq.Select( SessionColumnID.identifier(), SessionColumnCreationDate.identifier(), @@ -487,7 +486,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui ).From(sessionsTable.identifier()). LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)). LeftJoin(join(HumanUserIDCol, SessionColumnUserID)). - LeftJoin(join(UserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(UserIDCol, SessionColumnUserID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Sessions, error) { sessions := &Sessions{Sessions: []*Session{}} diff --git a/internal/query/sessions_test.go b/internal/query/sessions_test.go index ba897e6062..e0d9cfda71 100644 --- a/internal/query/sessions_test.go +++ b/internal/query/sessions_test.go @@ -50,8 +50,7 @@ var ( ` FROM projections.sessions8` + ` LEFT JOIN projections.login_names3 ON projections.sessions8.user_id = projections.login_names3.user_id AND projections.sessions8.instance_id = projections.login_names3.instance_id` + ` LEFT JOIN projections.users14_humans ON projections.sessions8.user_id = projections.users14_humans.user_id AND projections.sessions8.instance_id = projections.users14_humans.instance_id` + - ` LEFT JOIN projections.users14 ON projections.sessions8.user_id = projections.users14.id AND projections.sessions8.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.users14 ON projections.sessions8.user_id = projections.users14.id AND projections.sessions8.instance_id = projections.users14.instance_id`) expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions8.id,` + ` projections.sessions8.creation_date,` + ` projections.sessions8.change_date,` + @@ -81,8 +80,7 @@ var ( ` FROM projections.sessions8` + ` LEFT JOIN projections.login_names3 ON projections.sessions8.user_id = projections.login_names3.user_id AND projections.sessions8.instance_id = projections.login_names3.instance_id` + ` LEFT JOIN projections.users14_humans ON projections.sessions8.user_id = projections.users14_humans.user_id AND projections.sessions8.instance_id = projections.users14_humans.instance_id` + - ` LEFT JOIN projections.users14 ON projections.sessions8.user_id = projections.users14.id AND projections.sessions8.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.users14 ON projections.sessions8.user_id = projections.users14.id AND projections.sessions8.instance_id = projections.users14.instance_id`) sessionCols = []string{ "id", @@ -440,7 +438,7 @@ func Test_SessionsPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -577,14 +575,14 @@ func Test_SessionPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } -func prepareSessionQueryTesting(t *testing.T, token string) func(context.Context, prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Session, error)) { - return func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Session, error)) { - builder, scan := prepareSessionQuery(ctx, db) +func prepareSessionQueryTesting(t *testing.T, token string) func() (sq.SelectBuilder, func(*sql.Row) (*Session, error)) { + return func() (sq.SelectBuilder, func(*sql.Row) (*Session, error)) { + builder, scan := prepareSessionQuery() return builder, func(row *sql.Row) (*Session, error) { session, tokenID, err := scan(row) require.Equal(t, tokenID, token) diff --git a/internal/query/sms.go b/internal/query/sms.go index 310d3d0f14..3659f05daf 100644 --- a/internal/query/sms.go +++ b/internal/query/sms.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" @@ -149,7 +148,7 @@ func (q *Queries) SMSProviderConfigByID(ctx context.Context, id string) (config ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareSMSConfigQuery(ctx, q.client) + query, scan := prepareSMSConfigQuery() stmt, args, err := query.Where( sq.Eq{ SMSColumnID.identifier(): id, @@ -171,7 +170,7 @@ func (q *Queries) SMSProviderConfigActive(ctx context.Context, instanceID string ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareSMSConfigQuery(ctx, q.client) + query, scan := prepareSMSConfigQuery() stmt, args, err := query.Where( sq.Eq{ SMSColumnInstanceID.identifier(): instanceID, @@ -193,7 +192,7 @@ func (q *Queries) SearchSMSConfigs(ctx context.Context, queries *SMSConfigsSearc ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareSMSConfigsQuery(ctx, q.client) + query, scan := prepareSMSConfigsQuery() stmt, args, err := queries.toQuery(query). Where(sq.Eq{ SMSColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -217,7 +216,7 @@ func NewSMSProviderStateQuery(state domain.SMSConfigState) (SearchQuery, error) return NewNumberQuery(SMSColumnState, state, NumberEquals) } -func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*SMSConfig, error)) { +func prepareSMSConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMSConfig, error)) { return sq.Select( SMSColumnID.identifier(), SMSColumnAggregateID.identifier(), @@ -238,7 +237,7 @@ func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu SMSHTTPColumnEndpoint.identifier(), ).From(smsConfigsTable.identifier()). LeftJoin(join(SMSTwilioColumnSMSID, SMSColumnID)). - LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SMSConfig, error) { config := new(SMSConfig) @@ -281,7 +280,7 @@ func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } } -func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*SMSConfigs, error)) { +func prepareSMSConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SMSConfigs, error)) { return sq.Select( SMSColumnID.identifier(), SMSColumnAggregateID.identifier(), @@ -304,7 +303,7 @@ func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB countColumn.identifier(), ).From(smsConfigsTable.identifier()). LeftJoin(join(SMSTwilioColumnSMSID, SMSColumnID)). - LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Rows) (*SMSConfigs, error) { configs := &SMSConfigs{Configs: []*SMSConfig{}} diff --git a/internal/query/sms_test.go b/internal/query/sms_test.go index 82c3659f2c..e6e79d72bc 100644 --- a/internal/query/sms_test.go +++ b/internal/query/sms_test.go @@ -35,8 +35,7 @@ var ( ` projections.sms_configs3_http.endpoint` + ` FROM projections.sms_configs3` + ` LEFT JOIN projections.sms_configs3_twilio ON projections.sms_configs3.id = projections.sms_configs3_twilio.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_twilio.instance_id` + - ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id`) expectedSMSConfigsQuery = regexp.QuoteMeta(`SELECT projections.sms_configs3.id,` + ` projections.sms_configs3.aggregate_id,` + ` projections.sms_configs3.creation_date,` + @@ -59,8 +58,7 @@ var ( ` COUNT(*) OVER ()` + ` FROM projections.sms_configs3` + ` LEFT JOIN projections.sms_configs3_twilio ON projections.sms_configs3.id = projections.sms_configs3_twilio.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_twilio.instance_id` + - ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id`) smsConfigCols = []string{ "id", @@ -353,7 +351,7 @@ func Test_SMSConfigsPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -494,7 +492,7 @@ func Test_SMSConfigPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/smtp.go b/internal/query/smtp.go index 7c45fe33fe..4238ec121e 100644 --- a/internal/query/smtp.go +++ b/internal/query/smtp.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" @@ -153,7 +152,7 @@ func (q *Queries) SMTPConfigActive(ctx context.Context, resourceOwner string) (c ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareSMTPConfigQuery(ctx, q.client) + stmt, scan := prepareSMTPConfigQuery() query, args, err := stmt.Where(sq.Eq{ SMTPConfigColumnResourceOwner.identifier(): resourceOwner, SMTPConfigColumnInstanceID.identifier(): resourceOwner, @@ -174,7 +173,7 @@ func (q *Queries) SMTPConfigByID(ctx context.Context, instanceID, id string) (co ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareSMTPConfigQuery(ctx, q.client) + stmt, scan := prepareSMTPConfigQuery() query, args, err := stmt.Where(sq.Eq{ SMTPConfigColumnInstanceID.identifier(): instanceID, SMTPConfigColumnID.identifier(): id, @@ -190,7 +189,7 @@ func (q *Queries) SMTPConfigByID(ctx context.Context, instanceID, id string) (co return config, err } -func prepareSMTPConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*SMTPConfig, error)) { +func prepareSMTPConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMTPConfig, error)) { password := new(crypto.CryptoValue) return sq.Select( @@ -215,7 +214,7 @@ func prepareSMTPConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectB SMTPConfigHTTPColumnEndpoint.identifier()). From(smtpConfigsTable.identifier()). LeftJoin(join(SMTPConfigSMTPColumnID, SMTPConfigColumnID)). - LeftJoin(join(SMTPConfigHTTPColumnID, SMTPConfigColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(SMTPConfigHTTPColumnID, SMTPConfigColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SMTPConfig, error) { config := new(SMTPConfig) @@ -255,7 +254,7 @@ func prepareSMTPConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectB } } -func prepareSMTPConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*SMTPConfigs, error)) { +func prepareSMTPConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SMTPConfigs, error)) { return sq.Select( SMTPConfigColumnCreationDate.identifier(), SMTPConfigColumnChangeDate.identifier(), @@ -279,7 +278,7 @@ func prepareSMTPConfigsQuery(ctx context.Context, db prepareDatabase) (sq.Select countColumn.identifier(), ).From(smtpConfigsTable.identifier()). LeftJoin(join(SMTPConfigSMTPColumnID, SMTPConfigColumnID)). - LeftJoin(join(SMTPConfigHTTPColumnID, SMTPConfigColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(SMTPConfigHTTPColumnID, SMTPConfigColumnID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*SMTPConfigs, error) { configs := &SMTPConfigs{Configs: []*SMTPConfig{}} @@ -329,7 +328,7 @@ func (q *Queries) SearchSMTPConfigs(ctx context.Context, queries *SMTPConfigsSea ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareSMTPConfigsQuery(ctx, q.client) + query, scan := prepareSMTPConfigsQuery() stmt, args, err := queries.toQuery(query). Where(sq.Eq{ SMTPConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), diff --git a/internal/query/smtp_test.go b/internal/query/smtp_test.go index 4d12edcbd3..68ace249aa 100644 --- a/internal/query/smtp_test.go +++ b/internal/query/smtp_test.go @@ -33,8 +33,7 @@ var ( ` projections.smtp_configs5_http.endpoint` + ` FROM projections.smtp_configs5` + ` LEFT JOIN projections.smtp_configs5_smtp ON projections.smtp_configs5.id = projections.smtp_configs5_smtp.id AND projections.smtp_configs5.instance_id = projections.smtp_configs5_smtp.instance_id` + - ` LEFT JOIN projections.smtp_configs5_http ON projections.smtp_configs5.id = projections.smtp_configs5_http.id AND projections.smtp_configs5.instance_id = projections.smtp_configs5_http.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.smtp_configs5_http ON projections.smtp_configs5.id = projections.smtp_configs5_http.id AND projections.smtp_configs5.instance_id = projections.smtp_configs5_http.instance_id` prepareSMTPConfigCols = []string{ "creation_date", "change_date", @@ -287,7 +286,7 @@ func Test_SMTPConfigsPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/system_features.go b/internal/query/system_features.go index 31ad402d12..dcbbb7d6fe 100644 --- a/internal/query/system_features.go +++ b/internal/query/system_features.go @@ -25,7 +25,6 @@ type SystemFeatures struct { LegacyIntrospection FeatureSource[bool] UserSchema FeatureSource[bool] TokenExchange FeatureSource[bool] - Actions FeatureSource[bool] ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] OIDCSingleV1SessionTermination FeatureSource[bool] DisableUserTokenEvent FeatureSource[bool] diff --git a/internal/query/system_features_model.go b/internal/query/system_features_model.go index 217154e3ed..69e1f35968 100644 --- a/internal/query/system_features_model.go +++ b/internal/query/system_features_model.go @@ -60,7 +60,6 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v2.SystemLegacyIntrospectionEventType, feature_v2.SystemUserSchemaEventType, feature_v2.SystemTokenExchangeEventType, - feature_v2.SystemActionsEventType, feature_v2.SystemImprovedPerformanceEventType, feature_v2.SystemOIDCSingleV1SessionTerminationEventType, feature_v2.SystemDisableUserTokenEvent, @@ -82,7 +81,8 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S return err } switch key { - case feature.KeyUnspecified: + case feature.KeyUnspecified, + feature.KeyActionsDeprecated: return nil case feature.KeyLoginDefaultOrg: features.LoginDefaultOrg.set(level, event.Value) @@ -94,8 +94,6 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S features.UserSchema.set(level, event.Value) case feature.KeyTokenExchange: features.TokenExchange.set(level, event.Value) - case feature.KeyActions: - features.Actions.set(level, event.Value) case feature.KeyImprovedPerformance: features.ImprovedPerformance.set(level, event.Value) case feature.KeyOIDCSingleV1SessionTermination: diff --git a/internal/query/system_features_test.go b/internal/query/system_features_test.go index e460d38cec..5a58ac23d7 100644 --- a/internal/query/system_features_test.go +++ b/internal/query/system_features_test.go @@ -45,26 +45,22 @@ func TestQueries_GetSystemFeatures(t *testing.T) { name: "all features set", eventstore: expectEventstore( expectFilter( - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLegacyIntrospectionEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, true, - )), ), ), want: &SystemFeatures{ @@ -87,41 +83,33 @@ func TestQueries_GetSystemFeatures(t *testing.T) { Level: feature.LevelSystem, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, }, }, { name: "all features set, reset, set some feature", eventstore: expectEventstore( expectFilter( - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLegacyIntrospectionEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( context.Background(), aggregate, feature_v2.SystemResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, )), @@ -147,41 +135,33 @@ func TestQueries_GetSystemFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, { name: "all features set, reset, set some feature, not cascaded", eventstore: expectEventstore( expectFilter( - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLegacyIntrospectionEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( context.Background(), aggregate, feature_v2.SystemResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, )), @@ -207,10 +187,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, } diff --git a/internal/query/target.go b/internal/query/target.go index 03db85236c..d9b50f4a14 100644 --- a/internal/query/target.go +++ b/internal/query/target.go @@ -116,8 +116,8 @@ func (q *Queries) SearchTargets(ctx context.Context, queries *TargetSearchQuerie eq := sq.Eq{ TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } - query, scan := prepareTargetsQuery(ctx, q.client) - targets, err := genericRowsQueryWithState[*Targets](ctx, q.client, targetTable, combineToWhereStmt(query, queries.toQuery, eq), scan) + query, scan := prepareTargetsQuery() + targets, err := genericRowsQueryWithState(ctx, q.client, targetTable, combineToWhereStmt(query, queries.toQuery, eq), scan) if err != nil { return nil, err } @@ -134,8 +134,8 @@ func (q *Queries) GetTargetByID(ctx context.Context, id string) (*Target, error) TargetColumnID.identifier(): id, TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } - query, scan := prepareTargetQuery(ctx, q.client) - target, err := genericRowQuery[*Target](ctx, q.client, query.Where(eq), scan) + query, scan := prepareTargetQuery() + target, err := genericRowQuery(ctx, q.client, query.Where(eq), scan) if err != nil { return nil, err } @@ -153,7 +153,7 @@ func NewTargetInIDsSearchQuery(values []string) (SearchQuery, error) { return NewInTextQuery(TargetColumnID, values) } -func prepareTargetsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Targets, error)) { +func prepareTargetsQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*Targets, error)) { return sq.Select( TargetColumnID.identifier(), TargetColumnCreationDate.identifier(), @@ -205,7 +205,7 @@ func prepareTargetsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fu } } -func prepareTargetQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Target, error)) { +func prepareTargetQuery() (sq.SelectBuilder, func(row *sql.Row) (*Target, error)) { return sq.Select( TargetColumnID.identifier(), TargetColumnCreationDate.identifier(), diff --git a/internal/query/target_test.go b/internal/query/target_test.go index aa1ad517b7..ef564bf236 100644 --- a/internal/query/target_test.go +++ b/internal/query/target_test.go @@ -372,7 +372,7 @@ func Test_TargetPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/user.go b/internal/query/user.go index 3ee9a48463..c30eaaec74 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -13,7 +13,6 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" @@ -428,39 +427,11 @@ func (q *Queries) GetUserByLoginName(ctx context.Context, shouldTriggered bool, return user, err } -// Deprecated: use either GetUserByID or GetUserByLoginName -func (q *Queries) GetUser(ctx context.Context, shouldTriggerBulk bool, queries ...SearchQuery) (user *User, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - if shouldTriggerBulk { - triggerUserProjections(ctx) - } - - query, scan := prepareUserQuery(ctx, q.client) - for _, q := range queries { - query = q.toQuery(query) - } - eq := sq.Eq{ - UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), - } - stmt, args, err := query.Where(eq).ToSql() - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dnhr2", "Errors.Query.SQLStatment") - } - - err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { - user, err = scan(row) - return err - }, stmt, args...) - return user, err -} - func (q *Queries) GetHumanProfile(ctx context.Context, userID string, queries ...SearchQuery) (profile *Profile, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareProfileQuery(ctx, q.client) + query, scan := prepareProfileQuery() for _, q := range queries { query = q.toQuery(query) } @@ -484,7 +455,7 @@ func (q *Queries) GetHumanEmail(ctx context.Context, userID string, queries ...S ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareEmailQuery(ctx, q.client) + query, scan := prepareEmailQuery() for _, q := range queries { query = q.toQuery(query) } @@ -508,7 +479,7 @@ func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...S ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := preparePhoneQuery(ctx, q.client) + query, scan := preparePhoneQuery() for _, q := range queries { query = q.toQuery(query) } @@ -595,7 +566,7 @@ func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, queri triggerUserProjections(ctx) } - query, scan := prepareNotifyUserQuery(ctx, q.client) + query, scan := prepareNotifyUserQuery() for _, q := range queries { query = q.toQuery(query) } @@ -650,12 +621,15 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, f ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareUsersQuery(ctx, q.client) + query, scan := prepareUsersQuery() query = queries.toQuery(query).Where(sq.Eq{ UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), }) if permissionCheckV2 { - query = wherePermittedOrgsOrCurrentUser(ctx, query, filterOrgIds, UserResourceOwnerCol.identifier(), UserIDCol.identifier(), domain.PermissionUserRead) + query, err = wherePermittedOrgsOrCurrentUser(ctx, query, filterOrgIds, UserResourceOwnerCol.identifier(), UserIDCol.identifier(), domain.PermissionUserRead) + if err != nil { + return nil, zerrors.ThrowInternal(err, "AUTHZ-HS4us", "Errors.Internal") + } } stmt, args, err := query.ToSql() @@ -678,7 +652,7 @@ func (q *Queries) IsUserUnique(ctx context.Context, username, email, resourceOwn ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareUserUniqueQuery(ctx, q.client) + query, scan := prepareUserUniqueQuery() queries := make([]SearchQuery, 0, 3) if username != "" { usernameQuery, err := NewUserUsernameSearchQuery(username, TextEquals) @@ -737,15 +711,19 @@ func (r *UserSearchQueries) AppendMyResourceOwnerQuery(orgID string) error { func NewUserOrSearchQuery(values []SearchQuery) (SearchQuery, error) { return NewOrQuery(values...) } + func NewUserAndSearchQuery(values []SearchQuery) (SearchQuery, error) { return NewAndQuery(values...) } + func NewUserNotSearchQuery(value SearchQuery) (SearchQuery, error) { return NewNotQuery(value) } + func NewUserInUserIdsSearchQuery(values []string) (SearchQuery, error) { return NewInTextQuery(UserIDCol, values) } + func NewUserInUserEmailsSearchQuery(values []string) (SearchQuery, error) { return NewInTextQuery(HumanEmailCol, values) } @@ -807,7 +785,7 @@ func NewUserLoginNamesSearchQuery(value string) (SearchQuery, error) { } func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (SearchQuery, error) { - //linking queries for the subselect + // linking queries for the subselect instanceQuery, err := NewColumnComparisonQuery(LoginNameInstanceIDCol, UserInstanceIDCol, ColumnEquals) if err != nil { return nil, err @@ -816,12 +794,12 @@ func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (Searc if err != nil { return nil, err } - //text query to select data from the linked sub select + // text query to select data from the linked sub select loginNameQuery, err := NewTextQuery(LoginNameNameCol, value, comparison) if err != nil { return nil, err } - //full definition of the sub select + // full definition of the sub select subSelect, err := NewSubSelect(LoginNameUserIDCol, []SearchQuery{instanceQuery, userIDQuery, loginNameQuery}) if err != nil { return nil, err @@ -961,7 +939,7 @@ func scanUser(row *sql.Row) (*User, error) { return u, nil } -func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*User, error)) { +func prepareUserQuery() (sq.SelectBuilder, func(*sql.Row) (*User, error)) { loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() if err != nil { return sq.SelectBuilder{}, nil @@ -1012,14 +990,14 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder loginNamesArgs...). LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)), + userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), preferredLoginNameArgs...). PlaceholderFormat(sq.Dollar), scanUser } -func prepareProfileQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) { +func prepareProfileQuery() (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) { return sq.Select( UserIDCol.identifier(), UserCreationDateCol.identifier(), @@ -1035,7 +1013,7 @@ func prepareProfileQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil HumanGenderCol.identifier(), HumanAvatarURLCol.identifier()). From(userTable.identifier()). - LeftJoin(join(HumanUserIDCol, UserIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(HumanUserIDCol, UserIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Profile, error) { p := new(Profile) @@ -1085,7 +1063,7 @@ func prepareProfileQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } } -func prepareEmailQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Email, error)) { +func prepareEmailQuery() (sq.SelectBuilder, func(*sql.Row) (*Email, error)) { return sq.Select( UserIDCol.identifier(), UserCreationDateCol.identifier(), @@ -1096,7 +1074,7 @@ func prepareEmailQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde HumanEmailCol.identifier(), HumanIsEmailVerifiedCol.identifier()). From(userTable.identifier()). - LeftJoin(join(HumanUserIDCol, UserIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(HumanUserIDCol, UserIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Email, error) { e := new(Email) @@ -1132,7 +1110,7 @@ func prepareEmailQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde } } -func preparePhoneQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Phone, error)) { +func preparePhoneQuery() (sq.SelectBuilder, func(*sql.Row) (*Phone, error)) { return sq.Select( UserIDCol.identifier(), UserCreationDateCol.identifier(), @@ -1143,7 +1121,7 @@ func preparePhoneQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde HumanPhoneCol.identifier(), HumanIsPhoneVerifiedCol.identifier()). From(userTable.identifier()). - LeftJoin(join(HumanUserIDCol, UserIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(HumanUserIDCol, UserIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Phone, error) { e := new(Phone) @@ -1179,7 +1157,7 @@ func preparePhoneQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde } } -func prepareNotifyUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { +func prepareNotifyUserQuery() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() if err != nil { return sq.SelectBuilder{}, nil @@ -1224,7 +1202,7 @@ func prepareNotifyUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectB loginNamesArgs...). LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)), + userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), preferredLoginNameArgs...). PlaceholderFormat(sq.Dollar), scanNotifyUser @@ -1331,7 +1309,7 @@ func prepareCountUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (uint64, error) } } -func prepareUserUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (bool, error)) { +func prepareUserUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) { return sq.Select( UserIDCol.identifier(), UserStateCol.identifier(), @@ -1340,7 +1318,7 @@ func prepareUserUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectB HumanEmailCol.identifier(), HumanIsEmailVerifiedCol.identifier()). From(userTable.identifier()). - LeftJoin(join(HumanUserIDCol, UserIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(HumanUserIDCol, UserIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (bool, error) { userID := sql.NullString{} @@ -1368,7 +1346,7 @@ func prepareUserUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectB } } -func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { +func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() if err != nil { return sq.SelectBuilder{}, nil @@ -1417,7 +1395,7 @@ func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde loginNamesArgs...). LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)), + userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), preferredLoginNameArgs...). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Users, error) { diff --git a/internal/query/user_auth_method.go b/internal/query/user_auth_method.go index 0687545aef..8b26389f1a 100644 --- a/internal/query/user_auth_method.go +++ b/internal/query/user_auth_method.go @@ -3,6 +3,7 @@ package query import ( "context" "database/sql" + _ "embed" "errors" "slices" "time" @@ -11,7 +12,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -158,7 +158,7 @@ func (q *Queries) searchUserAuthMethods(ctx context.Context, queries *UserAuthMe ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareUserAuthMethodsQuery(ctx, q.client) + query, scan := prepareUserAuthMethodsQuery() stmt, args, err := queries.toQuery(query).Where(sq.Eq{UserAuthMethodColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-j9NJd", "Errors.Query.InvalidRequest") @@ -185,7 +185,7 @@ func (q *Queries) ListUserAuthMethodTypes(ctx context.Context, userID string, ac ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareUserAuthMethodTypesQuery(ctx, q.client, activeOnly, includeWithoutDomain, queryDomain) + query, scan := prepareUserAuthMethodTypesQuery(activeOnly, includeWithoutDomain, queryDomain) eq := sq.Eq{ UserIDCol.identifier(): userID, UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -212,6 +212,9 @@ type UserAuthMethodRequirements struct { ForceMFALocalOnly bool } +//go:embed user_auth_method_types_required.sql +var listUserAuthMethodTypesStmt string + func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID string) (requirements *UserAuthMethodRequirements, err error) { ctxData := authz.GetCtxData(ctx) if ctxData.UserID != userID { @@ -222,20 +225,33 @@ func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID st ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, q.client) - eq := sq.Eq{ - UserIDCol.identifier(): userID, - UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), - } - stmt, args, err := query.Where(eq).ToSql() - if err != nil { - return nil, zerrors.ThrowInvalidArgument(err, "QUERY-E5ut4", "Errors.Query.InvalidRequest") - } - - err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { - requirements, err = scan(row) - return err - }, stmt, args...) + err = q.client.QueryRowContext(ctx, + func(row *sql.Row) error { + var userType sql.NullInt32 + var forceMFA sql.NullBool + var forceMFALocalOnly sql.NullBool + err := row.Scan( + &userType, + &forceMFA, + &forceMFALocalOnly, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return zerrors.ThrowNotFound(err, "QUERY-SF3h2", "Errors.Internal") + } + return zerrors.ThrowInternal(err, "QUERY-Sf3rt", "Errors.Internal") + } + requirements = &UserAuthMethodRequirements{ + UserType: domain.UserType(userType.Int32), + ForceMFA: forceMFA.Bool, + ForceMFALocalOnly: forceMFALocalOnly.Bool, + } + return nil + }, + listUserAuthMethodTypesStmt, + userID, + authz.GetInstance(ctx).InstanceID(), + ) if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-Dun75", "Errors.Internal") } @@ -349,7 +365,7 @@ func (q *UserAuthMethodSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectB return query } -func prepareUserAuthMethodsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethods, error)) { +func prepareUserAuthMethodsQuery() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethods, error)) { return sq.Select( UserAuthMethodColumnTokenID.identifier(), UserAuthMethodColumnCreationDate.identifier(), @@ -361,7 +377,7 @@ func prepareUserAuthMethodsQuery(ctx context.Context, db prepareDatabase) (sq.Se UserAuthMethodColumnState.identifier(), UserAuthMethodColumnMethodType.identifier(), countColumn.identifier()). - From(userAuthMethodTable.identifier() + db.Timetravel(call.Took(ctx))). + From(userAuthMethodTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*AuthMethods, error) { userAuthMethods := make([]*AuthMethod, 0) @@ -399,7 +415,7 @@ func prepareUserAuthMethodsQuery(ctx context.Context, db prepareDatabase) (sq.Se } } -func prepareUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase, activeOnly bool, includeWithoutDomain bool, queryDomain string) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { +func prepareUserAuthMethodTypesQuery(activeOnly bool, includeWithoutDomain bool, queryDomain string) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { authMethodsQuery, authMethodsArgs, err := prepareAuthMethodQuery(activeOnly, includeWithoutDomain, queryDomain) if err != nil { return sq.SelectBuilder{}, nil @@ -420,7 +436,7 @@ func prepareUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase, ac authMethodsArgs...). LeftJoin("(" + idpsQuery + ") AS " + userIDPsCountTable.alias + " ON " + userIDPsCountUserID.identifier() + " = " + UserIDCol.identifier() + " AND " + - userIDPsCountInstanceID.identifier() + " = " + UserInstanceIDCol.identifier() + db.Timetravel(call.Took(ctx))). + userIDPsCountInstanceID.identifier() + " = " + UserInstanceIDCol.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*AuthMethodTypes, error) { userAuthMethodTypes := make([]domain.UserAuthMethodType, 0) @@ -461,45 +477,6 @@ func prepareUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase, ac } } -func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) { - loginPolicyQuery, err := prepareAuthMethodsForceMFAQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - return sq.Select( - UserTypeCol.identifier(), - forceMFAForce.identifier(), - forceMFAForceLocalOnly.identifier()). - From(userTable.identifier()). - LeftJoin("(" + loginPolicyQuery + ") AS " + forceMFATable.alias + " ON " + - "(" + forceMFAOrgID.identifier() + " = " + UserInstanceIDCol.identifier() + " OR " + forceMFAOrgID.identifier() + " = " + UserResourceOwnerCol.identifier() + ") AND " + - forceMFAInstanceID.identifier() + " = " + UserInstanceIDCol.identifier()). - OrderBy(forceMFAIsDefault.identifier()). - Limit(1). - PlaceholderFormat(sq.Dollar), - func(row *sql.Row) (*UserAuthMethodRequirements, error) { - var userType sql.NullInt32 - var forceMFA sql.NullBool - var forceMFALocalOnly sql.NullBool - err := row.Scan( - &userType, - &forceMFA, - &forceMFALocalOnly, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, zerrors.ThrowNotFound(err, "QUERY-SF3h2", "Errors.Internal") - } - return nil, zerrors.ThrowInternal(err, "QUERY-Sf3rt", "Errors.Internal") - } - return &UserAuthMethodRequirements{ - UserType: domain.UserType(userType.Int32), - ForceMFA: forceMFA.Bool, - ForceMFALocalOnly: forceMFALocalOnly.Bool, - }, nil - } -} - func prepareAuthMethodsIDPsQuery() (string, error) { idpsQuery, _, err := sq.Select( userIDPsCountUserID.identifier(), @@ -536,16 +513,3 @@ func prepareAuthMethodQuery(activeOnly bool, includeWithoutDomain bool, queryDom return q.ToSql() } - -func prepareAuthMethodsForceMFAQuery() (string, error) { - loginPolicyQuery, _, err := sq.Select( - forceMFAForce.identifier(), - forceMFAForceLocalOnly.identifier(), - forceMFAInstanceID.identifier(), - forceMFAOrgID.identifier(), - forceMFAIsDefault.identifier(), - ). - From(forceMFATable.identifier()). - ToSql() - return loginPolicyQuery, err -} diff --git a/internal/query/user_auth_method_test.go b/internal/query/user_auth_method_test.go index 041e4f8e9e..03f2e2174a 100644 --- a/internal/query/user_auth_method_test.go +++ b/internal/query/user_auth_method_test.go @@ -14,7 +14,6 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/zerrors" ) func TestUser_authMethodsCheckPermission(t *testing.T) { @@ -191,8 +190,7 @@ var ( ` projections.user_auth_methods5.state,` + ` projections.user_auth_methods5.method_type,` + ` COUNT(*) OVER ()` + - ` FROM projections.user_auth_methods5` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.user_auth_methods5` prepareUserAuthMethodsCols = []string{ "token_id", "creation_date", @@ -215,8 +213,7 @@ var ( ` ON auth_method_types.user_id = projections.users14.id AND auth_method_types.instance_id = projections.users14.instance_id` + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` + - ` ON user_idps_count.user_id = projections.users14.id AND user_idps_count.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms` + ` ON user_idps_count.user_id = projections.users14.id AND user_idps_count.instance_id = projections.users14.instance_id` prepareActiveAuthMethodTypesCols = []string{ "password_set", "method_type", @@ -232,8 +229,7 @@ var ( ` ON auth_method_types.user_id = projections.users14.id AND auth_method_types.instance_id = projections.users14.instance_id` + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` + - ` ON user_idps_count.user_id = projections.users14.id AND user_idps_count.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms` + ` ON user_idps_count.user_id = projections.users14.id AND user_idps_count.instance_id = projections.users14.instance_id` prepareActiveAuthMethodTypesDomainCols = []string{ "password_set", "method_type", @@ -249,8 +245,7 @@ var ( ` ON auth_method_types.user_id = projections.users14.id AND auth_method_types.instance_id = projections.users14.instance_id` + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` + - ` ON user_idps_count.user_id = projections.users14.id AND user_idps_count.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms` + ` ON user_idps_count.user_id = projections.users14.id AND user_idps_count.instance_id = projections.users14.instance_id` prepareActiveAuthMethodTypesDomainExternalCols = []string{ "password_set", "method_type", @@ -417,8 +412,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery no result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, true, "") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -434,8 +429,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery one second factor", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, true, "") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -466,8 +461,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery one second factor with domain", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "example.com") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, true, "example.com") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -498,8 +493,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery one second factor with domain external", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, false, "example.com") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, false, "example.com") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -530,8 +525,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery multiple second factors", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, true, "") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -568,8 +563,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery multiple second factors domain", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "example.com") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, true, "example.com") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -606,8 +601,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery multiple second factors domain external", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, false, "example.com") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, false, "example.com") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -644,8 +639,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, true, "") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -664,110 +659,10 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, object: (*AuthMethodTypes)(nil), }, - { - name: "prepareUserAuthMethodTypesRequiredQuery no result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) { - builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db) - return builder, func(row *sql.Row) (*UserAuthMethodRequirements, error) { - return scan(row) - } - }, - want: want{ - sqlExpectations: mockQueriesScanErr( - regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt), - nil, - nil, - ), - err: func(err error) (error, bool) { - if !zerrors.IsNotFound(err) { - return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false - } - return nil, true - }, - }, - object: (*UserAuthMethodRequirements)(nil), - }, - { - name: "prepareUserAuthMethodTypesRequiredQuery one second factor", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) { - builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db) - return builder, func(row *sql.Row) (*UserAuthMethodRequirements, error) { - return scan(row) - } - }, - want: want{ - sqlExpectations: mockQueries( - regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt), - prepareAuthMethodTypesRequiredCols, - [][]driver.Value{ - { - domain.UserTypeHuman, - true, - true, - }, - }, - ), - }, - object: &UserAuthMethodRequirements{ - UserType: domain.UserTypeHuman, - ForceMFA: true, - ForceMFALocalOnly: true, - }, - }, - { - name: "prepareUserAuthMethodTypesRequiredQuery multiple second factors", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) { - builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db) - return builder, func(row *sql.Row) (*UserAuthMethodRequirements, error) { - return scan(row) - } - }, - want: want{ - sqlExpectations: mockQueries( - regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt), - prepareAuthMethodTypesRequiredCols, - [][]driver.Value{ - { - domain.UserTypeHuman, - true, - true, - }, - }, - ), - }, - - object: &UserAuthMethodRequirements{ - UserType: domain.UserTypeHuman, - ForceMFA: true, - ForceMFALocalOnly: true, - }, - }, - { - name: "prepareUserAuthMethodTypesRequiredQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) { - builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db) - return builder, func(row *sql.Row) (*UserAuthMethodRequirements, error) { - return scan(row) - } - }, - want: want{ - sqlExpectations: mockQueryErr( - regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt), - sql.ErrConnDone, - ), - err: func(err error) (error, bool) { - if !errors.Is(err, sql.ErrConnDone) { - return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false - } - return nil, true - }, - }, - object: nil, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/user_auth_method_types_required.sql b/internal/query/user_auth_method_types_required.sql new file mode 100644 index 0000000000..d10420f0eb --- /dev/null +++ b/internal/query/user_auth_method_types_required.sql @@ -0,0 +1,17 @@ +SELECT + projections.users14.type + , auth_methods_force_mfa.force_mfa + , auth_methods_force_mfa.force_mfa_local_only +FROM + projections.users14 +LEFT JOIN + projections.login_policies5 AS auth_methods_force_mfa +ON + auth_methods_force_mfa.instance_id = projections.users14.instance_id + AND auth_methods_force_mfa.aggregate_id = ANY(ARRAY[projections.users14.instance_id, projections.users14.resource_owner]) +WHERE + projections.users14.id = $1 + AND projections.users14.instance_id = $2 +ORDER BY + auth_methods_force_mfa.is_default +LIMIT 1; \ No newline at end of file diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index 265d8eaae1..c3f24c066e 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -246,7 +245,7 @@ func (q *Queries) UserGrant(ctx context.Context, shouldTriggerBulk bool, queries traceSpan.EndWithError(err) } - query, scan := prepareUserGrantQuery(ctx, q.client) + query, scan := prepareUserGrantQuery() for _, q := range queries { query = q.toQuery(query) } @@ -274,7 +273,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh traceSpan.EndWithError(err) } - query, scan := prepareUserGrantsQuery(ctx, q.client) + query, scan := prepareUserGrantsQuery() eq := sq.Eq{UserGrantInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -298,7 +297,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh return grants, nil } -func prepareUserGrantQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserGrant, error)) { +func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, error)) { return sq.Select( UserGrantID.identifier(), UserGrantCreationDate.identifier(), @@ -336,7 +335,7 @@ func prepareUserGrantQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu LeftJoin(join(OrgColumnID, UserGrantResourceOwner)). LeftJoin(join(ProjectColumnID, UserGrantProjectID)). LeftJoin(join(GrantedOrgColumnId, UserResourceOwnerCol)). - LeftJoin(join(LoginNameUserIDCol, UserGrantUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), @@ -421,7 +420,7 @@ func prepareUserGrantQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } } -func prepareUserGrantsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, error)) { +func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, error)) { return sq.Select( UserGrantID.identifier(), UserGrantCreationDate.identifier(), @@ -461,7 +460,7 @@ func prepareUserGrantsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB LeftJoin(join(OrgColumnID, UserGrantResourceOwner)). LeftJoin(join(ProjectColumnID, UserGrantProjectID)). LeftJoin(join(GrantedOrgColumnId, UserResourceOwnerCol)). - LeftJoin(join(LoginNameUserIDCol, UserGrantUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/user_grant_test.go b/internal/query/user_grant_test.go index 6cfa0b563b..6a640c2ef2 100644 --- a/internal/query/user_grant_test.go +++ b/internal/query/user_grant_test.go @@ -47,7 +47,6 @@ var ( " LEFT JOIN projections.projects4 ON projections.user_grants5.project_id = projections.projects4.id AND projections.user_grants5.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.users14.resource_owner = granted_orgs.id AND projections.users14.instance_id = granted_orgs.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants5.user_id = projections.login_names3.user_id AND projections.user_grants5.instance_id = projections.login_names3.instance_id" + - ` AS OF SYSTEM TIME '-1 ms' ` + " WHERE projections.login_names3.is_primary = $1") userGrantCols = []string{ "id", @@ -110,7 +109,6 @@ var ( " LEFT JOIN projections.projects4 ON projections.user_grants5.project_id = projections.projects4.id AND projections.user_grants5.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.users14.resource_owner = granted_orgs.id AND projections.users14.instance_id = granted_orgs.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants5.user_id = projections.login_names3.user_id AND projections.user_grants5.instance_id = projections.login_names3.instance_id" + - ` AS OF SYSTEM TIME '-1 ms' ` + " WHERE projections.login_names3.is_primary = $1") userGrantsCols = append( userGrantCols, @@ -1008,7 +1006,7 @@ func Test_UserGrantPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/user_membership.go b/internal/query/user_membership.go index 7ba2629cfa..cae2b4dae3 100644 --- a/internal/query/user_membership.go +++ b/internal/query/user_membership.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -138,7 +137,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer wg.Wait() } - query, queryArgs, scan := prepareMembershipsQuery(ctx, q.client, queries) + query, queryArgs, scan := prepareMembershipsQuery(queries) eq := sq.Eq{membershipInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -237,7 +236,7 @@ func getMembershipFromQuery(queries *MembershipSearchQuery) (string, []interface args } -func prepareMembershipsQuery(ctx context.Context, db prepareDatabase, queries *MembershipSearchQuery) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Memberships, error)) { +func prepareMembershipsQuery(queries *MembershipSearchQuery) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Memberships, error)) { query, args := getMembershipFromQuery(queries) return sq.Select( membershipUserID.identifier(), @@ -259,7 +258,7 @@ func prepareMembershipsQuery(ctx context.Context, db prepareDatabase, queries *M LeftJoin(join(ProjectColumnID, membershipProjectID)). LeftJoin(join(OrgColumnID, membershipOrgID)). LeftJoin(join(ProjectGrantColumnGrantID, membershipGrantID)). - LeftJoin(join(InstanceColumnID, membershipInstanceID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(InstanceColumnID, membershipInstanceID)). PlaceholderFormat(sq.Dollar), args, func(rows *sql.Rows) (*Memberships, error) { diff --git a/internal/query/user_membership_test.go b/internal/query/user_membership_test.go index a0ea3cda31..b0170182d1 100644 --- a/internal/query/user_membership_test.go +++ b/internal/query/user_membership_test.go @@ -1,7 +1,6 @@ package query import ( - "context" "database/sql" "database/sql/driver" "errors" @@ -87,8 +86,7 @@ var ( " LEFT JOIN projections.projects4 ON members.project_id = projections.projects4.id AND members.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.orgs1 ON members.org_id = projections.orgs1.id AND members.instance_id = projections.orgs1.instance_id" + " LEFT JOIN projections.project_grants4 ON members.grant_id = projections.project_grants4.grant_id AND members.instance_id = projections.project_grants4.instance_id" + - " LEFT JOIN projections.instances ON members.instance_id = projections.instances.id" + - ` AS OF SYSTEM TIME '-1 ms'`) + " LEFT JOIN projections.instances ON members.instance_id = projections.instances.id") membershipCols = []string{ "user_id", "roles", @@ -456,14 +454,14 @@ func Test_MembershipPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } -func prepareMembershipWrapper() func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { - return func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { - builder, _, fun := prepareMembershipsQuery(ctx, db, &MembershipSearchQuery{}) +func prepareMembershipWrapper() func() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { + return func() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { + builder, _, fun := prepareMembershipsQuery(&MembershipSearchQuery{}) return builder, fun } } diff --git a/internal/query/user_metadata.go b/internal/query/user_metadata.go index a3b7c1fd34..ff612f82c8 100644 --- a/internal/query/user_metadata.go +++ b/internal/query/user_metadata.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -87,7 +86,7 @@ func (q *Queries) GetUserMetadataByKey(ctx context.Context, shouldTriggerBulk bo traceSpan.EndWithError(err) } - query, scan := prepareUserMetadataQuery(ctx, q.client) + query, scan := prepareUserMetadataQuery() for _, q := range queries { query = q.toQuery(query) } @@ -119,7 +118,7 @@ func (q *Queries) SearchUserMetadataForUsers(ctx context.Context, shouldTriggerB traceSpan.EndWithError(err) } - query, scan := prepareUserMetadataListQuery(ctx, q.client) + query, scan := prepareUserMetadataListQuery() eq := sq.Eq{ UserMetadataUserIDCol.identifier(): userIDs, UserMetadataInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -151,7 +150,7 @@ func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool traceSpan.EndWithError(err) } - query, scan := prepareUserMetadataListQuery(ctx, q.client) + query, scan := prepareUserMetadataListQuery() eq := sq.Eq{ UserMetadataUserIDCol.identifier(): userID, UserMetadataInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -235,7 +234,7 @@ func NewUserMetadataExistsQuery(key string, value []byte, keyComparison TextComp ) } -func prepareUserMetadataQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserMetadata, error)) { +func prepareUserMetadataQuery() (sq.SelectBuilder, func(*sql.Row) (*UserMetadata, error)) { return sq.Select( UserMetadataCreationDateCol.identifier(), UserMetadataChangeDateCol.identifier(), @@ -244,7 +243,7 @@ func prepareUserMetadataQuery(ctx context.Context, db prepareDatabase) (sq.Selec UserMetadataKeyCol.identifier(), UserMetadataValueCol.identifier(), ). - From(userMetadataTable.identifier() + db.Timetravel(call.Took(ctx))). + From(userMetadataTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*UserMetadata, error) { m := new(UserMetadata) @@ -267,7 +266,7 @@ func prepareUserMetadataQuery(ctx context.Context, db prepareDatabase) (sq.Selec } } -func prepareUserMetadataListQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserMetadataList, error)) { +func prepareUserMetadataListQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserMetadataList, error)) { return sq.Select( UserMetadataCreationDateCol.identifier(), UserMetadataChangeDateCol.identifier(), @@ -277,7 +276,7 @@ func prepareUserMetadataListQuery(ctx context.Context, db prepareDatabase) (sq.S UserMetadataKeyCol.identifier(), UserMetadataValueCol.identifier(), countColumn.identifier()). - From(userMetadataTable.identifier() + db.Timetravel(call.Took(ctx))). + From(userMetadataTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*UserMetadataList, error) { metadata := make([]*UserMetadata, 0) diff --git a/internal/query/user_metadata_test.go b/internal/query/user_metadata_test.go index 7f9d1b8ed3..6236272da4 100644 --- a/internal/query/user_metadata_test.go +++ b/internal/query/user_metadata_test.go @@ -18,8 +18,7 @@ var ( ` projections.user_metadata5.sequence,` + ` projections.user_metadata5.key,` + ` projections.user_metadata5.value` + - ` FROM projections.user_metadata5` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.user_metadata5` userMetadataCols = []string{ "creation_date", "change_date", @@ -251,7 +250,7 @@ func Test_UserMetadataPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/user_password.go b/internal/query/user_password.go index ed77d0d3ae..1d0037f721 100644 --- a/internal/query/user_password.go +++ b/internal/query/user_password.go @@ -119,7 +119,6 @@ func (wm *HumanPasswordReadModel) Reduce() error { func (wm *HumanPasswordReadModel) Query() *eventstore.SearchQueryBuilder { query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). AwaitOpenTransactions(). - AllowTimeTravel(). AddQuery(). AggregateTypes(user.AggregateType). AggregateIDs(wm.AggregateID). diff --git a/internal/query/user_personal_access_token.go b/internal/query/user_personal_access_token.go index dadd635b6a..8ea33f51a4 100644 --- a/internal/query/user_personal_access_token.go +++ b/internal/query/user_personal_access_token.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -98,7 +97,7 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk traceSpan.EndWithError(err) } - query, scan := preparePersonalAccessTokenQuery(ctx, q.client) + query, scan := preparePersonalAccessTokenQuery() for _, q := range queries { query = q.toQuery(query) } @@ -128,7 +127,7 @@ func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *Perso ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := preparePersonalAccessTokensQuery(ctx, q.client) + query, scan := preparePersonalAccessTokensQuery() eq := sq.Eq{ PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -178,7 +177,7 @@ func (q *PersonalAccessTokenSearchQueries) toQuery(query sq.SelectBuilder) sq.Se return query } -func preparePersonalAccessTokenQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*PersonalAccessToken, error)) { +func preparePersonalAccessTokenQuery() (sq.SelectBuilder, func(*sql.Row) (*PersonalAccessToken, error)) { return sq.Select( PersonalAccessTokenColumnID.identifier(), PersonalAccessTokenColumnCreationDate.identifier(), @@ -188,7 +187,7 @@ func preparePersonalAccessTokenQuery(ctx context.Context, db prepareDatabase) (s PersonalAccessTokenColumnUserID.identifier(), PersonalAccessTokenColumnExpiration.identifier(), PersonalAccessTokenColumnScopes.identifier()). - From(personalAccessTokensTable.identifier() + db.Timetravel(call.Took(ctx))). + From(personalAccessTokensTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*PersonalAccessToken, error) { p := new(PersonalAccessToken) @@ -212,7 +211,7 @@ func preparePersonalAccessTokenQuery(ctx context.Context, db prepareDatabase) (s } } -func preparePersonalAccessTokensQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*PersonalAccessTokens, error)) { +func preparePersonalAccessTokensQuery() (sq.SelectBuilder, func(*sql.Rows) (*PersonalAccessTokens, error)) { return sq.Select( PersonalAccessTokenColumnID.identifier(), PersonalAccessTokenColumnCreationDate.identifier(), @@ -223,7 +222,7 @@ func preparePersonalAccessTokensQuery(ctx context.Context, db prepareDatabase) ( PersonalAccessTokenColumnExpiration.identifier(), PersonalAccessTokenColumnScopes.identifier(), countColumn.identifier()). - From(personalAccessTokensTable.identifier() + db.Timetravel(call.Took(ctx))). + From(personalAccessTokensTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*PersonalAccessTokens, error) { personalAccessTokens := make([]*PersonalAccessToken, 0) diff --git a/internal/query/user_personal_access_token_test.go b/internal/query/user_personal_access_token_test.go index 79ba700ed5..dd3ed37e62 100644 --- a/internal/query/user_personal_access_token_test.go +++ b/internal/query/user_personal_access_token_test.go @@ -23,8 +23,7 @@ var ( " projections.personal_access_tokens3.user_id," + " projections.personal_access_tokens3.expiration," + " projections.personal_access_tokens3.scopes" + - " FROM projections.personal_access_tokens3" + - ` AS OF SYSTEM TIME '-1 ms'`) + " FROM projections.personal_access_tokens3") personalAccessTokenCols = []string{ "id", "creation_date", @@ -45,8 +44,7 @@ var ( " projections.personal_access_tokens3.expiration," + " projections.personal_access_tokens3.scopes," + " COUNT(*) OVER ()" + - " FROM projections.personal_access_tokens3" + - " AS OF SYSTEM TIME '-1 ms'") + " FROM projections.personal_access_tokens3") personalAccessTokensCols = []string{ "id", "creation_date", @@ -266,7 +264,7 @@ func Test_PersonalAccessTokenPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/user_schema.go b/internal/query/user_schema.go index ff5117d264..d3ea4dee79 100644 --- a/internal/query/user_schema.go +++ b/internal/query/user_schema.go @@ -96,7 +96,7 @@ func (q *Queries) GetUserSchemaByID(ctx context.Context, id string) (userSchema } query, scan := prepareUserSchemaQuery() - return genericRowQuery[*UserSchema](ctx, q.client, query.Where(eq), scan) + return genericRowQuery(ctx, q.client, query.Where(eq), scan) } func (q *Queries) SearchUserSchema(ctx context.Context, queries *UserSchemaSearchQueries) (userSchemas *UserSchemas, err error) { @@ -108,7 +108,7 @@ func (q *Queries) SearchUserSchema(ctx context.Context, queries *UserSchemaSearc } query, scan := prepareUserSchemasQuery() - return genericRowsQueryWithState[*UserSchemas](ctx, q.client, userSchemaTable, combineToWhereStmt(query, queries.toQuery, eq), scan) + return genericRowsQueryWithState(ctx, q.client, userSchemaTable, combineToWhereStmt(query, queries.toQuery, eq), scan) } func (q *UserSchemaSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { diff --git a/internal/query/user_test.go b/internal/query/user_test.go index 16b08611f0..50d65cc1ec 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -6,7 +6,6 @@ import ( "database/sql/driver" "errors" "fmt" - "reflect" "regexp" "testing" @@ -268,8 +267,7 @@ var ( ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` userCols = []string{ "id", "creation_date", @@ -319,8 +317,7 @@ var ( ` projections.users14_humans.gender,` + ` projections.users14_humans.avatar_key` + ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` profileCols = []string{ "id", "creation_date", @@ -345,8 +342,7 @@ var ( ` projections.users14_humans.email,` + ` projections.users14_humans.is_email_verified` + ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` emailCols = []string{ "id", "creation_date", @@ -366,8 +362,7 @@ var ( ` projections.users14_humans.phone,` + ` projections.users14_humans.is_phone_verified` + ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` phoneCols = []string{ "id", "creation_date", @@ -385,8 +380,7 @@ var ( ` projections.users14_humans.email,` + ` projections.users14_humans.is_email_verified` + ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` userUniqueCols = []string{ "id", "state", @@ -428,8 +422,7 @@ var ( ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` notifyUserCols = []string{ "id", "creation_date", @@ -497,8 +490,7 @@ var ( ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` usersCols = []string{ "id", "creation_date", @@ -1572,12 +1564,7 @@ func Test_UserPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - params := defaultPrepareArgs - if reflect.TypeOf(tt.prepare).NumIn() == 0 { - params = []reflect.Value{} - } - - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, params...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/userinfo_test.go b/internal/query/userinfo_test.go index 6ded7b4eed..5314283635 100644 --- a/internal/query/userinfo_test.go +++ b/internal/query/userinfo_test.go @@ -429,8 +429,7 @@ func TestQueries_GetOIDCUserInfo(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") @@ -476,8 +475,7 @@ func TestQueries_GetOIDCUserinfoClientByID(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") diff --git a/internal/query/web_key_test.go b/internal/query/web_key_test.go index 6008ec6528..80d07bfa13 100644 --- a/internal/query/web_key_test.go +++ b/internal/query/web_key_test.go @@ -208,8 +208,7 @@ func TestQueries_GetActiveSigningWebKey(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, keyEncryptionAlgorithm: alg, } @@ -307,8 +306,7 @@ func TestQueries_ListWebKeys(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } got, err := q.ListWebKeys(ctx) @@ -369,8 +367,7 @@ func TestQueries_GetWebKeySet(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } got, err := q.GetWebKeySet(ctx) diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go index 42ccc98936..ada6a2191a 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -3,7 +3,7 @@ package renderer import ( "context" "html/template" - "io/ioutil" + "io" "net/http" "os" @@ -107,7 +107,7 @@ func (r *Renderer) addFileToTemplate(dir http.FileSystem, tmpl *template.Templat return err } defer f.Close() - content, err := ioutil.ReadAll(f) + content, err := io.ReadAll(f) if err != nil { return err } diff --git a/internal/repository/authrequest/auth_request.go b/internal/repository/authrequest/auth_request.go index 99f034333b..75624e3a21 100644 --- a/internal/repository/authrequest/auth_request.go +++ b/internal/repository/authrequest/auth_request.go @@ -38,6 +38,7 @@ type AddedEvent struct { LoginHint *string `json:"login_hint,omitempty"` HintUserID *string `json:"hint_user_id,omitempty"` NeedRefreshToken bool `json:"need_refresh_token,omitempty"` + Issuer string `json:"issuer,omitempty"` } func (e *AddedEvent) Payload() interface{} { @@ -66,6 +67,7 @@ func NewAddedEvent(ctx context.Context, loginHint, hintUserID *string, needRefreshToken bool, + issuer string, ) *AddedEvent { return &AddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -89,6 +91,7 @@ func NewAddedEvent(ctx context.Context, LoginHint: loginHint, HintUserID: hintUserID, NeedRefreshToken: needRefreshToken, + Issuer: issuer, } } diff --git a/internal/repository/execution/queue.go b/internal/repository/execution/queue.go new file mode 100644 index 0000000000..ed3a6ce4a0 --- /dev/null +++ b/internal/repository/execution/queue.go @@ -0,0 +1,71 @@ +package execution + +import ( + "encoding/json" + "time" + + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + QueueName = "execution" +) + +type Request struct { + Aggregate *eventstore.Aggregate `json:"aggregate"` + Sequence uint64 `json:"sequence"` + EventType eventstore.EventType `json:"eventType"` + CreatedAt time.Time `json:"createdAt"` + UserID string `json:"userID"` + EventData []byte `json:"eventData"` + TargetsData []byte `json:"targetsData"` +} + +func (e *Request) Kind() string { + return "execution_request" +} + +func ContextInfoFromRequest(e *Request) *ContextInfoEvent { + return &ContextInfoEvent{ + AggregateID: e.Aggregate.ID, + AggregateType: string(e.Aggregate.Type), + ResourceOwner: e.Aggregate.ResourceOwner, + InstanceID: e.Aggregate.InstanceID, + Version: string(e.Aggregate.Version), + Sequence: e.Sequence, + EventType: string(e.EventType), + CreatedAt: e.CreatedAt.Format(time.RFC3339Nano), + UserID: e.UserID, + EventPayload: e.EventData, + } +} + +type ContextInfoEvent struct { + AggregateID string `json:"aggregateID,omitempty"` + AggregateType string `json:"aggregateType,omitempty"` + ResourceOwner string `json:"resourceOwner,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + Version string `json:"version,omitempty"` + Sequence uint64 `json:"sequence,omitempty"` + EventType string `json:"event_type,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UserID string `json:"userID,omitempty"` + EventPayload json.RawMessage `json:"event_payload,omitempty"` +} + +func (c *ContextInfoEvent) GetHTTPRequestBody() []byte { + data, err := json.Marshal(c) + if err != nil { + return nil + } + return data +} + +func (c *ContextInfoEvent) SetHTTPResponseBody(resp []byte) error { + // response is irrelevant and will not be unmarshaled + return nil +} + +func (c *ContextInfoEvent) GetContent() any { + return c.EventPayload +} diff --git a/internal/repository/feature/feature_v2/eventstore.go b/internal/repository/feature/feature_v2/eventstore.go index e8d0da1ab0..00618f56c2 100644 --- a/internal/repository/feature/feature_v2/eventstore.go +++ b/internal/repository/feature/feature_v2/eventstore.go @@ -12,7 +12,6 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, SystemLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]]) - eventstore.RegisterFilterEventMapper(AggregateType, SystemActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemOIDCSingleV1SessionTerminationEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemDisableUserTokenEvent, eventstore.GenericEventMapper[SetEvent[bool]]) @@ -26,7 +25,6 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, InstanceLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]]) - eventstore.RegisterFilterEventMapper(AggregateType, InstanceActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceWebKeyEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceDebugOIDCParentErrorEventType, eventstore.GenericEventMapper[SetEvent[bool]]) diff --git a/internal/repository/feature/feature_v2/feature.go b/internal/repository/feature/feature_v2/feature.go index 008986824b..d5e8941df2 100644 --- a/internal/repository/feature/feature_v2/feature.go +++ b/internal/repository/feature/feature_v2/feature.go @@ -17,7 +17,6 @@ var ( SystemLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLegacyIntrospection) SystemUserSchemaEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyUserSchema) SystemTokenExchangeEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTokenExchange) - SystemActionsEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyActions) SystemImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyImprovedPerformance) SystemOIDCSingleV1SessionTerminationEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyOIDCSingleV1SessionTermination) SystemDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelSystem, feature.KeyDisableUserTokenEvent) @@ -31,7 +30,6 @@ var ( InstanceLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLegacyIntrospection) InstanceUserSchemaEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyUserSchema) InstanceTokenExchangeEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTokenExchange) - InstanceActionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyActions) InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance) InstanceWebKeyEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyWebKey) InstanceDebugOIDCParentErrorEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDebugOIDCParentError) diff --git a/internal/repository/permission/eventstore.go b/internal/repository/permission/eventstore.go new file mode 100644 index 0000000000..65a56d1b74 --- /dev/null +++ b/internal/repository/permission/eventstore.go @@ -0,0 +1,8 @@ +package permission + +import "github.com/zitadel/zitadel/internal/eventstore" + +func init() { + eventstore.RegisterFilterEventMapper(AggregateType, AddedType, eventstore.GenericEventMapper[AddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, RemovedType, eventstore.GenericEventMapper[RemovedEvent]) +} diff --git a/internal/repository/samlrequest/saml_request.go b/internal/repository/samlrequest/saml_request.go index b3ecdd753e..aca8da99fe 100644 --- a/internal/repository/samlrequest/saml_request.go +++ b/internal/repository/samlrequest/saml_request.go @@ -19,14 +19,15 @@ const ( type AddedEvent struct { *eventstore.BaseEvent `json:"-"` - LoginClient string `json:"login_client,omitempty"` - ApplicationID string `json:"application_id,omitempty"` - ACSURL string `json:"acs_url,omitempty"` - RelayState string `json:"relay_state,omitempty"` - RequestID string `json:"request_id,omitempty"` - Binding string `json:"binding,omitempty"` - Issuer string `json:"issuer,omitempty"` - Destination string `json:"destination,omitempty"` + LoginClient string `json:"login_client,omitempty"` + ApplicationID string `json:"application_id,omitempty"` + ACSURL string `json:"acs_url,omitempty"` + RelayState string `json:"relay_state,omitempty"` + RequestID string `json:"request_id,omitempty"` + Binding string `json:"binding,omitempty"` + Issuer string `json:"issuer,omitempty"` + Destination string `json:"destination,omitempty"` + ResponseIssuer string `json:"response_issuer,omitempty"` } func (e *AddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { @@ -51,6 +52,7 @@ func NewAddedEvent(ctx context.Context, binding string, issuer string, destination string, + responseIssuer string, ) *AddedEvent { return &AddedEvent{ BaseEvent: eventstore.NewBaseEventForPush( @@ -58,14 +60,15 @@ func NewAddedEvent(ctx context.Context, aggregate, AddedType, ), - LoginClient: loginClient, - ApplicationID: applicationID, - ACSURL: acsURL, - RelayState: relayState, - RequestID: requestID, - Binding: binding, - Issuer: issuer, - Destination: destination, + LoginClient: loginClient, + ApplicationID: applicationID, + ACSURL: acsURL, + RelayState: relayState, + RequestID: requestID, + Binding: binding, + Issuer: issuer, + Destination: destination, + ResponseIssuer: responseIssuer, } } diff --git a/internal/static/database/crdb.go b/internal/static/database/crdb.go index a031f7d17a..549e0ae505 100644 --- a/internal/static/database/crdb.go +++ b/internal/static/database/crdb.go @@ -14,7 +14,7 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -var _ static.Storage = (*crdbStorage)(nil) +var _ static.Storage = (*storage)(nil) const ( assetsTable = "system.assets" @@ -29,15 +29,15 @@ const ( AssetColUpdatedAt = "updated_at" ) -type crdbStorage struct { +type storage struct { client *sql.DB } func NewStorage(client *sql.DB, _ map[string]interface{}) (static.Storage, error) { - return &crdbStorage{client: client}, nil + return &storage{client: client}, nil } -func (c *crdbStorage) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) { +func (c *storage) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) { data, err := io.ReadAll(object) if err != nil { return nil, zerrors.ThrowInternal(err, "DATAB-Dfwvq", "Errors.Internal") @@ -71,7 +71,7 @@ func (c *crdbStorage) PutObject(ctx context.Context, instanceID, location, resou }, nil } -func (c *crdbStorage) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) { +func (c *storage) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) { query, args, err := squirrel.Select(AssetColData, AssetColContentType, AssetColHash, AssetColUpdatedAt). From(assetsTable). Where(squirrel.Eq{ @@ -111,7 +111,7 @@ func (c *crdbStorage) GetObject(ctx context.Context, instanceID, resourceOwner, nil } -func (c *crdbStorage) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*static.Asset, error) { +func (c *storage) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*static.Asset, error) { query, args, err := squirrel.Select(AssetColContentType, AssetColLocation, "length("+AssetColData+")", AssetColHash, AssetColUpdatedAt). From(assetsTable). Where(squirrel.Eq{ @@ -143,7 +143,7 @@ func (c *crdbStorage) GetObjectInfo(ctx context.Context, instanceID, resourceOwn return asset, nil } -func (c *crdbStorage) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error { +func (c *storage) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error { stmt, args, err := squirrel.Delete(assetsTable). Where(squirrel.Eq{ AssetColInstanceID: instanceID, @@ -162,7 +162,7 @@ func (c *crdbStorage) RemoveObject(ctx context.Context, instanceID, resourceOwne return nil } -func (c *crdbStorage) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error { +func (c *storage) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error { stmt, args, err := squirrel.Delete(assetsTable). Where(squirrel.Eq{ AssetColInstanceID: instanceID, @@ -181,7 +181,7 @@ func (c *crdbStorage) RemoveObjects(ctx context.Context, instanceID, resourceOwn return nil } -func (c *crdbStorage) RemoveInstanceObjects(ctx context.Context, instanceID string) error { +func (c *storage) RemoveInstanceObjects(ctx context.Context, instanceID string) error { stmt, args, err := squirrel.Delete(assetsTable). Where(squirrel.Eq{ AssetColInstanceID: instanceID, diff --git a/internal/static/database/crdb_test.go b/internal/static/database/crdb_test.go index 14a128dbe2..2be76e69fa 100644 --- a/internal/static/database/crdb_test.go +++ b/internal/static/database/crdb_test.go @@ -40,7 +40,7 @@ const ( " WHERE instance_id = $1" ) -func Test_crdbStorage_CreateObject(t *testing.T) { +func Test_dbStorage_CreateObject(t *testing.T) { type fields struct { client db } @@ -112,7 +112,7 @@ func Test_crdbStorage_CreateObject(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &crdbStorage{ + c := &storage{ client: tt.fields.client.db, } got, err := c.PutObject(tt.args.ctx, tt.args.instanceID, tt.args.location, tt.args.resourceOwner, tt.args.name, tt.args.contentType, tt.args.objectType, tt.args.data, tt.args.objectSize) @@ -127,7 +127,7 @@ func Test_crdbStorage_CreateObject(t *testing.T) { } } -func Test_crdbStorage_RemoveObject(t *testing.T) { +func Test_dbStorage_RemoveObject(t *testing.T) { type fields struct { client db } @@ -166,7 +166,7 @@ func Test_crdbStorage_RemoveObject(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &crdbStorage{ + c := &storage{ client: tt.fields.client.db, } err := c.RemoveObject(tt.args.ctx, tt.args.instanceID, tt.args.resourceOwner, tt.args.name) @@ -178,7 +178,7 @@ func Test_crdbStorage_RemoveObject(t *testing.T) { } } -func Test_crdbStorage_RemoveObjects(t *testing.T) { +func Test_dbStorage_RemoveObjects(t *testing.T) { type fields struct { client db } @@ -216,7 +216,7 @@ func Test_crdbStorage_RemoveObjects(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &crdbStorage{ + c := &storage{ client: tt.fields.client.db, } err := c.RemoveObjects(tt.args.ctx, tt.args.instanceID, tt.args.resourceOwner, tt.args.objectType) @@ -227,7 +227,7 @@ func Test_crdbStorage_RemoveObjects(t *testing.T) { }) } } -func Test_crdbStorage_RemoveInstanceObjects(t *testing.T) { +func Test_dbStorage_RemoveInstanceObjects(t *testing.T) { type fields struct { client db } @@ -260,7 +260,7 @@ func Test_crdbStorage_RemoveInstanceObjects(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &crdbStorage{ + c := &storage{ client: tt.fields.client.db, } err := c.RemoveInstanceObjects(tt.args.ctx, tt.args.instanceID) diff --git a/internal/static/i18n/ro.yaml b/internal/static/i18n/ro.yaml new file mode 100644 index 0000000000..48790da9e5 --- /dev/null +++ b/internal/static/i18n/ro.yaml @@ -0,0 +1,1413 @@ +Errors: + Internal: A apărut o eroare internă + NoChangesFound: Nicio modificare + OriginNotAllowed: Acest "Origin" nu este permis + IDMissing: ID lipsă + ResourceOwnerMissing: Organizația Proprietarului Resursei lipsă + RemoveFailed: Nu a putut fi eliminat + ProjectionName: + Invalid: Nume de proiecție invalid + Assets: + EmptyKey: Cheia activului este goală + Store: + NotInitialized: Stocarea activelor nu este inițializată + NotConfigured: Stocarea activelor nu este configurată + Bucket: + Internal: Eroare internă la crearea bucket-ului + AlreadyExists: Bucket-ul există deja + CreateFailed: Bucket-ul nu a fost creat + ListFailed: Bucket-urile nu au putut fi citite + RemoveFailed: Bucket-ul nu a fost șters + SetPublicFailed: Nu s-a putut seta bucket-ul ca public + Object: + PutFailed: Obiectul nu a fost creat + GetFailed: Obiectul nu a putut fi citit + NotFound: Obiectul nu a fost găsit + PresignedTokenFailed: Token-ul semnat nu a putut fi creat + ListFailed: Lista de obiecte nu a putut fi citită + RemoveFailed: Obiectul nu a putut fi eliminat + Limit: + ExceedsDefault: Limita depășește limita implicită + Limits: + NotFound: Limitele nu au fost găsite + NoneSpecified: Nu au fost specificate limite + Instance: + Blocked: Instanța este blocată + Restrictions: + NoneSpecified: Nu au fost specificate restricții + DefaultLanguageMustBeAllowed: Limba implicită trebuie să fie permisă + Language: + NotParsed: Nu s-a putut analiza limba + NotSupported: Limba nu este suportată + NotAllowed: Limba nu este permisă + Undefined: Limba este nedefinită + Duplicate: Există duplicate în liste de limbi + OIDCSettings: + NotFound: Configurația OIDC nu a fost găsită + AlreadyExists: Configurația OIDC există deja + SecretGenerator: + AlreadyExists: Generatorul de secrete există deja + TypeMissing: Tipul generatorului de secrete lipsește + NotFound: Generatorul de secrete nu a fost găsit + SMSConfig: + NotFound: Configurația SMS nu a fost găsită + AlreadyActive: Configurația SMS este deja activă + AlreadyDeactivated: Configurația SMS este deja dezactivată + NotExternalVerification: Configurația SMS nu suportă verificarea prin cod + SMTP: + NotEmailMessage: Mesajul nu este EmailMessage + RequiredAttributes: Subiectul, destinatarii și conținutul trebuie să fie setate, dar unele sau toate sunt goale + CouldNotSplit: Nu s-a putut împărți host și port pentru conectarea la smtp + CouldNotDial: Nu s-a putut contacta serverul SMTP, verificați portul, probleme de firewall... + CouldNotDialTLS: Nu s-a putut contacta serverul SMTP folosind TLS, verificați portul, probleme de firewall... + CouldNotCreateClient: Nu s-a putut crea clientul smtp + CouldNotStartTLS: Nu s-a putut porni tls + CouldNotAuth: Nu s-a putut adăuga autentificare smtp, verificați dacă numele de utilizator și parola sunt corecte, dacă sunt corecte, poate furnizorul dvs. necesită o metodă de autentificare nesuportată de ZITADEL + CouldNotSetSender: Nu s-a putut seta expeditorul + CouldNotSetRecipient: Nu s-a putut seta destinatarul + SMTPConfig: + TestPassword: Parola pentru test nu a fost găsită + NotFound: Configurația SMTP nu a fost găsită + AlreadyExists: Configurația SMTP există deja + AlreadyDeactivated: Configurația SMTP este deja dezactivată + SenderAdressNotCustomDomain: Adresa expeditorului trebuie configurată ca domeniu personalizat pe instanță. + TestEmailNotFound: Adresa de e-mail pentru test nu a fost găsită + Notification: + NoDomain: Niciun domeniu găsit pentru mesaj + User: + NotFound: Utilizatorul nu a putut fi găsit + AlreadyExists: Utilizatorul există deja + NotFoundOnOrg: Utilizatorul nu a putut fi găsit în organizația aleasă + NotAllowedOrg: Utilizatorul nu este membru al organizației cerute + UserIDMissing: ID-ul utilizatorului lipsește + UserIDWrong: "Utilizatorul din cerere nu este egal cu utilizatorul autentificat" + DomainPolicyNil: Politica organizației este goală + EmailAsUsernameNotAllowed: E-mailul nu este permis ca nume de utilizator + Invalid: Datele utilizatorului sunt invalide + DomainNotAllowedAsUsername: Domeniul este deja rezervat și nu poate fi folosit + AlreadyInactive: Utilizatorul este deja inactiv + NotInactive: Utilizatorul nu este inactiv + CantDeactivateInitial: Utilizatorul cu starea inițială poate fi doar șters, nu dezactivat + ShouldBeActiveOrInitial: Utilizatorul nu este activ sau inițial + AlreadyInitialised: Utilizatorul este deja inițializat + NotInitialised: Utilizatorul nu este încă inițializat + NotLocked: Utilizatorul nu este blocat + NoChanges: Nu au fost găsite modificări + InitCodeNotFound: Codul de inițializare nu a fost găsit + UsernameNotChanged: Numele de utilizator nu a fost schimbat + InvalidURLTemplate: Șablonul URL este invalid + Profile: + NotFound: Profilul nu a fost găsit + NotChanged: Profilul nu a fost schimbat + Empty: Profilul este gol + FirstNameEmpty: Prenumele în profil este gol + LastNameEmpty: Numele de familie în profil este gol + IDMissing: ID-ul profilului lipsește + Email: + NotFound: E-mailul nu a fost găsit + Invalid: E-mailul este invalid + AlreadyVerified: E-mailul este deja verificat + NotChanged: E-mailul nu a fost schimbat + Empty: E-mailul este gol + IDMissing: ID-ul e-mailului lipsește + Phone: + NotFound: Numărul de telefon nu a fost găsit + Invalid: Numărul de telefon este invalid + AlreadyVerified: Numărul de telefon este deja verificat + Empty: Numărul de telefon este gol + NotChanged: Numărul de telefon nu a fost schimbat + Address: + NotFound: Adresa nu a fost găsită + NotChanged: Adresa nu a fost schimbată + Machine: + Key: + NotFound: Cheia mașinii nu a fost găsită + AlreadyExisting: Cheia mașinii există deja + Invalid: Cheia publică nu este o cheie publică RSA validă în format PKIX cu codificare PEM + Secret: + NotExisting: Secretul nu există + Invalid: Secretul este invalid + CouldNotGenerate: Secretul nu a putut fi generat + PAT: + NotFound: Token-ul de acces personal nu a fost găsit + NotHuman: Utilizatorul trebuie să fie personal + NotMachine: Utilizatorul trebuie să fie tehnic + WrongType: Nu este permis pentru acest tip de utilizator + NotAllowedToLink: Utilizatorului nu i se permite să se conecteze cu un furnizor de autentificare extern + Username: + AlreadyExists: Numele de utilizator este deja folosit + Reserved: Numele de utilizator este deja luat + Empty: Numele de utilizator este gol + Code: + Empty: Codul este gol + NotFound: Codul nu a fost găsit + Expired: Codul a expirat + GeneratorAlgNotSupported: Algoritm de generator neacceptat + Invalid: Codul este invalid + Password: + NotFound: Parola nu a fost găsită + Empty: Parola este goală + Invalid: Parola este invalidă + NotSet: Utilizatorul nu a setat o parolă + NotChanged: Parola nouă nu poate fi aceeași cu parola curentă + NotSupported: Codificarea hash a parolei nu este acceptată. Consultați https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets + PasswordComplexityPolicy: + NotFound: Politica de parolă nu a fost găsită + MinLength: Parola este prea scurtă + MinLengthNotAllowed: Lungimea minimă dată nu este permisă + HasLower: Parola trebuie să conțină litere mici + HasUpper: Parola trebuie să conțină litere mari + HasNumber: Parola trebuie să conțină numere + HasSymbol: Parola trebuie să conțină simboluri + ExternalIDP: + Invalid: IDP extern invalid + IDPConfigNotExisting: Furnizorul IDP este invalid pentru această organizație + NotAllowed: IDP extern nepermis + MinimumExternalIDPNeeded: Trebuie adăugat cel puțin un IDP + AlreadyExists: IDP extern deja luat + NotFound: IDP extern nu a fost găsit + LoginFailed: Conectarea la IDP extern a eșuat + MFA: + OTP: + AlreadyReady: Multifactor OTP (Parolă Unică) este deja configurat + NotExisting: Multifactor OTP (Parolă Unică) nu există + NotReady: Multifactor OTP (Parolă Unică) nu este pregătit + InvalidCode: Cod invalid + U2F: + NotExisting: U2F nu există + Passwordless: + NotExisting: Fără parolă nu există + WebAuthN: + NotFound: Token-ul WebAuthN nu a putut fi găsit + BeginRegisterFailed: Înregistrarea WebAuthN a început, dar a eșuat + MarshalError: Eroare la serializarea datelor + ErrorOnParseCredential: Eroare la analiza datelor de acreditare + CreateCredentialFailed: Eroare la crearea acreditărilor + BeginLoginFailed: Autentificarea WebAuthN a început, dar a eșuat + ValidateLoginFailed: Eroare la validarea acreditărilor de autentificare + CloneWarning: Acreditările pot fi clonate + RefreshToken: + Invalid: Token-ul de reîmprospătare este invalid + NotFound: Token-ul de reîmprospătare nu a fost găsit + Instance: + NotFound: Instanța nu a fost găsită + AlreadyExists: Instanța există deja + NotChanged: Instanța nu a fost schimbată + Org: + AlreadyExists: Numele organizației este deja luat + Invalid: Organizația este invalidă + AlreadyDeactivated: Organizația este deja dezactivată + AlreadyActive: Organizația este deja activă + Empty: Organizația este goală + NotFound: Organizația nu a fost găsită + NotChanged: Organizația nu a fost schimbată + DefaultOrgNotDeletable: Organizația implicită nu trebuie să fie ștearsă + ZitadelOrgNotDeletable: Organizația cu proiectul ZITADEL nu trebuie să fie ștearsă + InvalidDomain: Domeniu invalid + DomainMissing: Domeniu lipsă + DomainNotOnOrg: Domeniul nu există în organizație + DomainNotVerified: Domeniul nu este verificat + DomainAlreadyVerified: Domeniul este deja verificat + DomainVerificationTypeInvalid: Tipul de verificare a domeniului este invalid + DomainVerificationMissing: Verificarea domeniului nu a fost încă începută + DomainVerificationFailed: Verificarea domeniului a eșuat + DomainVerificationTXTNotFound: Înregistrarea TXT _zitadel-challenge nu a fost găsită pentru domeniul dvs. Verificați dacă l-ați adăugat la serverul DNS sau așteptați până când noua înregistrare este propagată + DomainVerificationTXTNoMatch: Înregistrarea TXT _zitadel-challenge a fost găsită pentru domeniul dvs., dar nu conține textul corect al token-ului. Verificați dacă ați adăugat token-ul corect la serverul DNS sau așteptați până când noua înregistrare este propagată + DomainVerificationHTTPNotFound: Fișierul care conține provocarea nu a fost găsit în URL-ul așteptat. Verificați dacă ați încărcat fișierul în locul potrivit cu permisiuni de citire + DomainVerificationHTTPNoMatch: Fișierul care conține provocarea a fost găsit în URL-ul așteptat, dar nu conține textul corect al token-ului. Verificați conținutul acestuia + DomainVerificationTimeout: A existat o depășire a timpului de așteptare la interogarea serverului DNS + PrimaryDomainNotDeletable: Domeniul principal nu trebuie să fie șters + DomainNotFound: Domeniul nu a fost găsit + MemberIDMissing: ID-ul membrului lipsește + MemberNotFound: Membrul organizației nu a fost găsit + InvalidMember: Membrul organizației este invalid + UserIDMissing: ID-ul utilizatorului lipsește + PolicyAlreadyExists: Politica există deja + PolicyNotExisting: Politica nu există + IdpInvalid: Configurația IDP este invalidă + IdpNotExisting: Configurația IDP nu există + OIDCConfigInvalid: Configurația OIDC IDP este invalidă + IdpIsNotOIDC: Configurația IDP nu este de tip oidc + Domain: + AlreadyExists: Domeniul există deja + InvalidCharacter: Numai caractere alfanumerice, . și - sunt permise pentru un domeniu + EmptyString: Caracterele non-numerice și alfabetice invalide au fost înlocuite cu spații goale și domeniul rezultat este un șir gol + IDP: + InvalidSearchQuery: Interogare de căutare invalidă + ClientIDMissing: ClientID lipsă + TeamIDMissing: TeamID lipsă + KeyIDMissing: KeyID lipsă + PrivateKeyMissing: Cheie Privată lipsă + LoginPolicy: + NotFound: Politica de conectare nu a fost găsită + Invalid: Politica de conectare este invalidă + RedirectURIInvalid: URI-ul de redirecționare implicit este invalid + NotExisting: Politica de conectare nu există + AlreadyExists: Politica de conectare există deja + IdpProviderAlreadyExisting: Furnizorul de identitate există deja + IdpProviderNotExisting: Furnizorul de identitate nu există + RegistrationNotAllowed: Înregistrarea nu este permisă + UsernamePasswordNotAllowed: Conectarea cu nume de utilizator / parolă nu este permisă + MFA: + AlreadyExists: Multifactor există deja + NotExisting: Multifactor nu există + Unspecified: Multifactor invalid + MailTemplate: + NotFound: Șablonul de e-mail implicit nu a fost găsit + NotChanged: Șablonul de e-mail implicit nu a fost schimbat + AlreadyExists: Șablonul de e-mail implicit există deja + Invalid: Șablonul de e-mail implicit este invalid + CustomMessageText: + NotFound: Textul mesajului implicit nu a fost găsit + NotChanged: Textul mesajului implicit nu a fost schimbat + AlreadyExists: Textul mesajului implicit există deja + Invalid: Textul mesajului implicit este invalid + PasswordComplexityPolicy: + NotFound: Politica de complexitate a parolei nu a fost găsită + Empty: Politica de complexitate a parolei este goală + NotExisting: Politica de complexitate a parolei nu există + AlreadyExists: Politica de complexitate a parolei există deja + PasswordLockoutPolicy: + NotFound: Politica de blocare a parolei nu a fost găsită + Empty: Politica de blocare a parolei este goală + NotExisting: Politica de blocare a parolei nu există + AlreadyExists: Politica de blocare a parolei există deja + PasswordAgePolicy: + NotFound: Politica de vârstă a parolei nu a fost găsită + Empty: Politica de vârstă a parolei este goală + NotExisting: Politica de vârstă a parolei nu există + AlreadyExists: Politica de vârstă a parolei există deja + OrgIAMPolicy: + Empty: Politica IAM a organizației este goală + NotExisting: Politica IAM a organizației nu există + AlreadyExists: Politica IAM a organizației există deja + NotificationPolicy: + NotFound: Politica de notificare nu a fost găsită + NotChanged: Politica de notificare nu a fost schimbată + AlreadyExists: Politica de notificare există deja + LabelPolicy: + NotFound: Politica de etichete private nu a fost găsită + NotChanged: Politica de etichete private nu a fost schimbată + Project: + ProjectIDMissing: ID-ul proiectului lipsește + AlreadyExists: Proiectul există deja în organizație + OrgNotExisting: Organizația nu există + UserNotExisting: Utilizatorul nu există + CouldNotGenerateClientSecret: Nu s-a putut genera secretul clientului + Invalid: Proiectul este invalid + NotActive: Proiectul nu este activ + NotInactive: Proiectul nu este dezactivat + NotFound: Proiectul nu a fost găsit + UserIDMissing: ID-ul utilizatorului lipsește + Member: + NotFound: Membrul proiectului nu a fost găsit + Invalid: Membrul proiectului este invalid + AlreadyExists: Membrul proiectului există deja + NotExisting: Membrul proiectului nu există + MinimumOneRoleNeeded: Trebuie adăugat cel puțin un rol + Role: + AlreadyExists: Rolul există deja + Invalid: Rolul este invalid + NotExisting: Rolul nu există + IDMissing: ID lipsă + App: + AlreadyExists: Aplicația există deja + NotFound: Aplicația nu a fost găsită + Invalid: Aplicația este invalidă + NotExisting: Aplicația nu există + NotActive: Aplicația nu este activă + NotInactive: Aplicația nu este inactivă + OIDCConfigInvalid: Configurația OIDC este invalidă + APIConfigInvalid: Configurația API este invalidă + SAMLConfigInvalid: Configurația SAML este invalidă + IsNotOIDC: Aplicația nu este de tip OIDC + IsNotAPI: Aplicația nu este de tip API + IsNotSAML: Aplicația nu este de tip SAML + SAMLMetadataMissing: Lipsesc metadatele SAML + SAMLMetadataFormat: Eroare de formatare a metadatelor SAML + SAMLEntityIDAlreadyExisting: SAML EntityID există deja + OIDCAuthMethodNoSecret: Metoda de autentificare OIDC aleasă nu necesită un secret + APIAuthMethodNoSecret: Metoda de autentificare API aleasă nu necesită un secret + AuthMethodNoPrivateKeyJWT: Metoda de autentificare aleasă nu necesită o cheie + ClientSecretInvalid: Secretul clientului este invalid + Key: + AlreadyExisting: Cheia aplicației există deja + NotFound: Cheia aplicației nu a fost găsită + RequiredFieldsMissing: Unele câmpuri obligatorii lipsesc + Grant: + AlreadyExists: Acordarea proiectului există deja + NotFound: Acordarea nu a fost găsită + Invalid: Acordarea proiectului este invalidă + NotExisting: Acordarea proiectului nu există + HasNotExistingRole: Un rol nu există în proiect + NotActive: Acordarea proiectului nu este activă + NotInactive: Acordarea proiectului nu este inactivă + IAM: + NotFound: Instanța nu a fost găsită. Asigurați-vă că aveți domeniul corect. Consultați https://zitadel.com/docs/apis/introduction#domains + Member: + RolesNotChanged: Rolurile nu au fost schimbate + MemberInvalid: Membrul este invalid + MemberAlreadyExisting: Membrul există deja + MemberNotExisting: Membrul nu există + IDMissing: Id lipsă + IAMProjectIDMissing: ID-ul proiectului IAM lipsește + IamProjectAlreadySet: ID-ul proiectului IAM a fost deja setat + IdpInvalid: Configurația IDP este invalidă + IdpNotExisting: Configurația IDP nu există + OIDCConfigInvalid: Configurația OIDC IDP este invalidă + IdpIsNotOIDC: Configurația IDP nu este de tip oidc + LoginPolicyInvalid: Politica de conectare este invalidă + LoginPolicyNotExisting: Politica de conectare nu există + IdpProviderInvalid: Furnizorul de identitate este invalid + LoginPolicy: + NotFound: Politica de conectare implicită nu a fost găsită + NotChanged: Politica de conectare implicită nu a fost schimbată + NotExisting: Politica de conectare implicită nu există + AlreadyExists: Politica de conectare implicită există deja + RedirectURIInvalid: URI-ul de redirecționare implicit este invalid + MFA: + AlreadyExists: Multifactor există deja + NotExisting: Multifactor nu există + Unspecified: Multifactor invalid + IDP: + AlreadyExists: Furnizorul de identitate există deja + NotExisting: Furnizorul de identitate nu există + Invalid: Furnizorul de identitate este invalid + IDPConfig: + AlreadyExists: Configurația furnizorului de identitate există deja + NotInactive: Configurația furnizorului de identitate nu este inactivă + NotActive: Configurația furnizorului de identitate nu este activă + LabelPolicy: + NotFound: Politica de etichete private implicită nu a fost găsită + NotChanged: Politica de etichete private implicită nu a fost schimbată + MailTemplate: + NotFound: Șablonul de e-mail implicit nu a fost găsit + NotChanged: Șablonul de e-mail implicit nu a fost schimbat + AlreadyExists: Șablonul de e-mail implicit există deja + Invalid: Șablonul de e-mail implicit este invalid + CustomMessageText: + NotFound: Textul mesajului implicit nu a fost găsit + NotChanged: Textul mesajului implicit nu a fost schimbat + AlreadyExists: Textul mesajului implicit există deja + Invalid: Textul mesajului implicit este invalid + PasswordComplexityPolicy: + NotFound: Politica implicită de complexitate a parolei nu a fost găsită + NotExisting: Politica implicită de complexitate a parolei nu există + AlreadyExists: Politica implicită de complexitate a parolei există deja + Empty: Politica implicită de complexitate a parolei este goală + NotChanged: Politica implicită de complexitate a parolei nu a fost schimbată + PasswordAgePolicy: + NotFound: Politica implicită de vârstă a parolei nu a fost găsită + NotExisting: Politica implicită de vârstă a parolei nu există + AlreadyExists: Politica implicită de vârstă a parolei există deja + Empty: Politica implicită de vârstă a parolei este goală + NotChanged: Politica implicită de vârstă a parolei nu a fost schimbată + PasswordLockoutPolicy: + NotFound: Politica implicită de blocare a parolei nu a fost găsită + NotExisting: Politica implicită de blocare a parolei nu există + AlreadyExists: Politica implicită de blocare a parolei există deja + Empty: Politica implicită de blocare a parolei este goală + NotChanged: Politica implicită de blocare a parolei nu a fost schimbată + DomainPolicy: + NotFound: Politica IAM a organizației nu a fost găsită + Empty: Politica IAM a organizației este goală + NotExisting: Politica IAM a organizației nu există + AlreadyExists: Politica IAM a organizației există deja + NotChanged: Politica IAM a organizației nu a fost schimbată + NotificationPolicy: + NotFound: Politica de notificare implicită nu a fost găsită + NotChanged: Politica de notificare implicită nu a fost schimbată + AlreadyExists: Politica de notificare implicită există deja + Policy: + AlreadyExists: Politica există deja + Label: + Invalid: + PrimaryColor: Culoarea primară nu este o valoare de culoare Hex validă + BackgroundColor: Culoarea de fundal nu este o valoare de culoare Hex validă + WarnColor: Culoarea de avertizare nu este o valoare de culoare Hex validă + FontColor: Culoarea fontului nu este o valoare de culoare Hex validă + PrimaryColorDark: Culoarea primară (modul întunecat) nu este o valoare de culoare Hex validă + BackgroundColorDark: Culoarea de fundal (modul întunecat) nu este o valoare de culoare Hex validă + WarnColorDark: Culoarea de avertizare (modul întunecat) nu este o valoare de culoare Hex validă + FontColorDark: Culoarea fontului (modul întunecat) nu este o valoare de culoare Hex validă + UserGrant: + AlreadyExists: Acordarea utilizatorului există deja + NotFound: Acordarea utilizatorului nu a fost găsită + Invalid: Acordarea utilizatorului este invalidă + NotChanged: Acordarea utilizatorului nu a fost schimbată + IDMissing: Id lipsă + NotActive: Acordarea utilizatorului nu este activă + NotInactive: Acordarea utilizatorului nu este dezactivată + NoPermissionForProject: Utilizatorul nu are permisiuni pentru acest proiect + RoleKeyNotFound: Rolul nu a fost găsit + Member: + AlreadyExists: Membrul există deja + IDPConfig: + AlreadyExists: Configurația IDP cu acest nume există deja + NotExisting: Configurația furnizorului de identitate nu există + Changes: + NotFound: Niciun istoric găsit + AuditRetention: Istoricul este în afara perioadei de păstrare a jurnalului de audit + Token: + NotFound: Token-ul nu a fost găsit + Invalid: Token-ul este invalid + UserSession: + NotFound: Sesiunea utilizatorului nu a fost găsită + Key: + NotFound: Cheia nu a fost găsită + ExpireBeforeNow: Data de expirare este în trecut + Login: + LoginPolicy: + MFA: + ForceAndNotConfigured: Multifactor este configurat ca obligatoriu, dar nu sunt configurați furnizori posibili. Vă rugăm să contactați administratorul sistemului. + Step: + Started: + AlreadyExists: Pasul a început deja, există deja + Done: + AlreadyExists: Pasul a terminat deja, există deja + CustomText: + AlreadyExists: Textul personalizat există deja + Invalid: Textul personalizat este invalid + NotFound: Textul personalizat nu a fost găsit + TranslationFile: + ReadError: Eroare la citirea fișierului de traducere + MergeError: Fișierul de traducere nu a putut fi îmbinat cu traducerile personalizate + NotFound: Fișierul de traducere nu există + Metadata: + NotFound: Metadatele nu au fost găsite + NoData: Lista de metadate este goală + Invalid: Metadatele sunt invalide + KeyNotExisting: Una sau mai multe chei nu există + Action: + Invalid: Acțiunea este invalidă + NotFound: Acțiunea nu a fost găsită + NotActive: Acțiunea nu este activă + NotInactive: Acțiunea nu este inactivă + MaxAllowed: Nu sunt permise acțiuni active suplimentare + NotEnabled: Caracteristica "Acțiune" nu este activată + Flow: + FlowTypeMissing: FlowType lipsă + Empty: Fluxul este deja gol + WrongTriggerType: TriggerType este invalid + NoChanges: Nicio modificare + ActionIDsNotExist: ActionIDs nu există + Query: + CloseRows: Declarația SQL nu a putut fi terminată + SQLStatement: Declarația SQL nu a putut fi creată + InvalidRequest: Cererea este invalidă + TooManyNestingLevels: Prea multe niveluri de imbricare a interogărilor (Max 20) + LimitExceeded: Limita a fost depășită + Quota: + AlreadyExists: Cota există deja pentru această unitate + NotFound: Cota nu a fost găsită pentru această unitate + Invalid: + CallURL: URL-ul de apel al cotei este invalid + Percent: Procentul cotei este mai mic de 1 + Unimplemented: Cotele nu sunt implementate pentru această unitate + Amount: Suma cotei este mai mică de 1 + ResetInterval: Intervalul de resetare a cotei este mai scurt de un minut + Noop: O cotă nelimitată fără notificări nu are efect + Access: + Exhausted: Cota pentru cererile autentificate este epuizată + Execution: + Exhausted: Cota pentru secunde de execuție este epuizată + LogStore: + Access: + StorageFailed: Stocarea jurnalului de acces în baza de date a eșuat + ScanFailed: Interogarea utilizării pentru cererile autentificate a eșuat + Execution: + StorageFailed: Stocarea jurnalului de execuție a acțiunii în baza de date a eșuat + ScanFailed: Interogarea utilizării pentru secunde de execuție a acțiunii a eșuat + Session: + NotExisting: Sesiunea nu există + Terminated: Sesiunea a fost deja terminată + Expired: Sesiunea a expirat + PositiveLifetime: Durata de viață a sesiunii nu trebuie să fie mai mică de 0 + Token: + Invalid: Token-ul de sesiune este invalid + WebAuthN: + NoChallenge: Sesiune fără provocare WebAuthN + Intent: + IDPMissing: ID-ul IDP lipsește în cerere + IDPInvalid: IDP invalid pentru cerere + ResponseInvalid: Răspunsul IDP este invalid + MissingSingleMappingAttribute: Răspunsul IDP nu conține atributul de mapare sau are mai mult de o valoare + SuccessURLMissing: URL-ul de succes lipsește în cerere + FailureURLMissing: URL-ul de eșec lipsește în cerere + StateMissing: Parametrul de stare lipsește în cerere + NotStarted: Intenția nu este pornită sau a fost deja terminată + NotSucceeded: Intenția nu a reușit + TokenCreationFailed: Crearea token-ului a eșuat + InvalidToken: Token-ul intenției este invalid + OtherUser: Intenția este destinată altui utilizator + AuthRequest: + AlreadyExists: Cererea de autentificare există deja + NotExisting: Cererea de autentificare nu există + WrongLoginClient: Cererea de autentificare a fost creată de alt client de autentificare + OIDCSession: + RefreshTokenInvalid: Token-ul de reîmprospătare este invalid + Token: + Invalid: Token-ul este invalid + Expired: Token-ul a expirat + InvalidClient: Token-ul nu a fost emis pentru acest client + SAMLRequest: + AlreadyExists: Cererea SAML există deja + NotExisting: Cererea SAML nu există + WrongLoginClient: Cererea SAML a fost creată de alt client de autentificare + SAMLSession: + InvalidClient: Răspunsul SAML nu a fost emis pentru acest client + Feature: + NotExisting: Caracteristica nu există + TypeNotSupported: Tipul caracteristicii nu este suportat + InvalidValue: Valoare invalidă pentru această caracteristică + Target: + Invalid: Ținta este invalidă + NoTimeout: Ținta nu are timp de așteptare + InvalidURL: Ținta are un URL invalid + NotFound: Ținta nu a fost găsită + Execution: + ConditionInvalid: Condiția de execuție este invalidă + Invalid: Execuția este invalidă + NotFound: Execuția nu a fost găsită + IncludeNotFound: Includerea nu a fost găsită + NoTargets: Nu sunt definite ținte + Failed: Execuția a eșuat + ResponseIsNotValidJSON: Răspunsul nu este un JSON valid + UserSchema: + NotEnabled: Caracteristica "Schema de utilizator" nu este activată + Type: + Missing: Tipul schemei de utilizator lipsește + AlreadyExists: Tipul schemei de utilizator există deja + Authenticator: + Invalid: Tip de autentificator invalid + NotActive: Schema de utilizator nu este activă + NotInactive: Schema de utilizator nu este inactivă + NotExists: Schema de utilizator nu există + ID: + Missing: ID-ul schemei de utilizator lipsește + Invalid: Schema de utilizator este invalidă + Data: + Invalid: Datele sunt invalide pentru schema de utilizator + TokenExchange: + FeatureDisabled: Caracteristica Token Exchange este dezactivată pentru instanța dvs. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features + Token: + Missing: Token-ul lipsește + Invalid: Token-ul este invalid + TypeMissing: Tipul token-ului lipsește + TypeNotAllowed: Tipul token-ului nu este permis + TypeNotSupported: Tipul token-ului nu este suportat + NotForAPI: Token-urile impersonate nu sunt permise pentru API + Impersonation: + PolicyDisabled: Impersonarea este dezactivată în politica de securitate a instanței + WebKey: + ActiveDelete: Nu se poate șterge o cheie web activă + Config: Configurație cheie web invalidă + Duplicate: ID-ul cheii web nu este unic + FeatureDisabled: Caracteristica cheie web este dezactivată + NoActive: Nu a fost găsită nicio cheie web activă + NotFound: Cheia web nu a fost găsită + + AggregateTypes: + action: Acțiune + instance: Instanță + key_pair: Pereche de chei + org: Organizație + project: Proiect + user: Utilizator + usergrant: Acordare de utilizator + quota: Cotă + feature: Caracteristică + target: Țintă + execution: Execuție + user_schema: Schema de utilizator + auth_request: Cerere de autentificare + device_auth: Autentificare dispozitiv + idpintent: Intenție IDP + limits: Limite + milestone: Piatră de hotar + oidc_session: Sesiune OIDC + restrictions: Restricții + system: Sistem + session: Sesiune + web_key: Cheie web + saml_request: Cerere SAML + saml_session: Sesiune SAML + + EventTypes: + execution: + set: Execuție setată + removed: Execuție ștearsă + target: + added: Țintă creată + changed: Țintă schimbată + removed: Țintă ștearsă + user: + added: Utilizator adăugat + selfregistered: Utilizator s-a înregistrat singur + initialization: + code: + added: Cod de inițializare generat + sent: Cod de inițializare trimis + check: + succeeded: Verificarea inițializării a reușit + failed: Verificarea inițializării a eșuat + token: + added: Token de acces creat + v2.added: Token de acces creat + removed: Token de acces șters + impersonated: Utilizator impersonat + username: + reserved: Nume de utilizator rezervat + released: Nume de utilizator eliberat + changed: Nume de utilizator schimbat + email: + reserved: Adresă de e-mail rezervată + released: Adresă de e-mail eliberată + changed: Adresă de e-mail schimbată + verified: Adresă de e-mail verificată + verification: + failed: Verificarea adresei de e-mail a eșuat + code: + added: Cod de verificare a adresei de e-mail generat + sent: Cod de verificare a adresei de e-mail trimis + machine: + added: Utilizator tehnic adăugat + changed: Utilizator tehnic schimbat + key: + added: Cheie adăugată + removed: Cheie ștearsă + secret: + set: Secret setat + updated: Hash secret actualizat + removed: Secret șters + check: + succeeded: Verificarea secretului a reușit + failed: Verificarea secretului a eșuat + human: + added: Persoană adăugată + selfregistered: Persoană s-a înregistrat singură + avatar: + added: Avatar adăugat + removed: Avatar șters + initialization: + code: + added: Cod de inițializare generat + sent: Cod de inițializare trimis + check: + succeeded: Verificarea inițializării a reușit + failed: Verificarea inițializării a eșuat + invite: + code: + added: Cod de invitație generat + sent: Cod de invitație trimis + check: + succeeded: Verificarea invitației a reușit + failed: Verificarea invitației a eșuat + username: + reserved: Nume de utilizator rezervat + released: Nume de utilizator eliberat + email: + changed: Adresă de e-mail schimbată + verified: Adresă de e-mail verificată + verification: + failed: Verificarea adresei de e-mail a eșuat + code: + added: Cod de verificare a adresei de e-mail generat + sent: Cod de verificare a adresei de e-mail trimis + password: + changed: Parolă schimbată + code: + added: Cod de parolă generat + sent: Cod de parolă trimis + check: + succeeded: Verificarea parolei a reușit + failed: Verificarea parolei a eșuat + change: + sent: Schimbare de parolă trimisă + hash: + updated: Hash de parolă actualizat + externallogin: + check: + succeeded: Autentificare externă a reușit + externalidp: + added: IDP extern adăugat + removed: IDP extern șters + cascade: + removed: IDP extern cascadă șters + id: + migrated: UserID extern al IDP a fost migrat + phone: + changed: Număr de telefon schimbat + verified: Număr de telefon verificat + verification: + failed: Verificarea numărului de telefon a eșuat + code: + added: Cod de număr de telefon generat + sent: Cod de număr de telefon trimis + removed: Număr de telefon șters + profile: + changed: Profil de utilizator schimbat + address: + changed: Adresă de utilizator schimbată + mfa: + otp: + added: Multifactor OTP adăugat + verified: Multifactor OTP verificat + removed: Multifactor OTP șters + check: + succeeded: Verificarea Multifactor OTP a reușit + failed: Verificarea Multifactor OTP a eșuat + sms: + added: Multifactor OTP SMS adăugat + removed: Multifactor OTP SMS șters + code: + added: Cod Multifactor OTP SMS adăugat + sent: Cod Multifactor OTP SMS trimis + check: + succeeded: Verificarea Multifactor OTP SMS a reușit + failed: Verificarea Multifactor OTP SMS a eșuat + email: + added: Multifactor OTP Email adăugat + removed: Multifactor OTP Email șters + code: + added: Cod Multifactor OTP Email adăugat + sent: Cod Multifactor OTP Email trimis + check: + succeeded: Verificarea Multifactor OTP Email a reușit + failed: Verificarea Multifactor OTP Email a eșuat + u2f: + token: + added: Token pentru Multifactor U2F adăugat + verified: Token pentru Multifactor U2F verificat + removed: Token pentru Multifactor U2F șters + begin: + login: Verificarea Multifactor U2F a început + check: + succeeded: Verificarea Multifactor U2F a reușit + failed: Verificarea Multifactor U2F a eșuat + signcount: + changed: Suma de control a token-ului Multifactor U2F a fost schimbată + init: + skipped: Inițializarea Multifactor a fost omisă + passwordless: + token: + added: Token pentru conectare fără parolă adăugat + verified: Token pentru conectare fără parolă verificat + removed: Token pentru conectare fără parolă șters + begin: + login: Verificarea conectării fără parolă a început + check: + succeeded: Verificarea conectării fără parolă a reușit + failed: Verificarea conectării fără parolă a eșuat + signcount: + changed: Suma de control a token-ului de conectare fără parolă a fost schimbată + initialization: + code: + added: Cod de inițializare fără parolă adăugat + sent: Cod de inițializare fără parolă trimis + requested: Cod de inițializare fără parolă solicitat + check: + succeeded: Codul de inițializare fără parolă a fost verificat cu succes + failed: Verificarea codului de inițializare fără parolă a eșuat + signed: + out: Utilizator deconectat + refresh: + token: + added: Token de reîmprospătare creat + renewed: Token de reîmprospătare reînnoit + removed: Token de reîmprospătare șters + locked: Utilizator blocat + unlocked: Utilizator deblocat + deactivated: Utilizator dezactivat + reactivated: Utilizator reactivat + removed: Utilizator șters + password: + changed: Parolă schimbată + code: + added: Cod de parolă generat + sent: Cod de parolă trimis + check: + succeeded: Verificarea parolei a reușit + failed: Verificarea parolei a eșuat + phone: + changed: Număr de telefon schimbat + verified: Număr de telefon verificat + verification: + failed: Verificarea numărului de telefon a eșuat + code: + added: Cod de număr de telefon generat + sent: Cod de număr de telefon trimis + + profile: + changed: Profil de utilizator schimbat + address: + changed: Adresă de utilizator schimbată + mfa: + otp: + added: Multifactor OTP adăugat + verified: Multifactor OTP verificat + removed: Multifactor OTP șters + check: + succeeded: Verificarea Multifactor OTP a reușit + failed: Verificarea Multifactor OTP a eșuat + init: + skipped: Inițializarea Multifactor OTP a fost omisă + init: + skipped: Inițializarea Multifactor a fost omisă + signed: + out: Utilizator deconectat + grant: + added: Autorizație adăugată + changed: Autorizație schimbată + removed: Autorizație ștearsă + deactivated: Autorizație dezactivată + reactivated: Autorizație reactivată + reserved: Autorizație rezervată + released: Autorizație eliberată + cascade: + removed: Autorizație ștearsă + changed: Autorizație schimbată + metadata: + set: Metadate de utilizator setate + removed: Metadate de utilizator șterse + removed.all: Toate metadatele de utilizator au fost șterse + domain: + claimed: Domeniu revendicat + claimed.sent: Notificarea de revendicare a domeniului a fost trimisă + pat: + added: Token de acces personal adăugat + removed: Token de acces personal șters + org: + added: Organizație adăugată + changed: Organizație schimbată + deactivated: Organizație dezactivată + reactivated: Organizație reactivată + removed: Organizație ștearsă + domain: + added: Domeniu adăugat + verification: + added: Verificare de domeniu adăugată + failed: Verificare de domeniu eșuată + verified: Domeniu verificat + removed: Domeniu șters + primary: + set: Domeniu principal setat + reserved: Domeniu rezervat + released: Domeniu eliberat + name: + reserved: Nume de organizație rezervat + released: Nume de organizație eliberat + member: + added: Membru al organizației adăugat + changed: Membru al organizației schimbat + removed: Membru al organizației șters + cascade: + removed: Membru al organizației cascadă șters + iam: + policy: + added: Politică de sistem adăugată + changed: Politică de sistem schimbată + removed: Politică de sistem ștearsă + idp: + config: + added: Configurație IDP adăugată + changed: Configurație IDP schimbată + removed: Configurație IDP ștearsă + deactivated: Configurație IDP dezactivată + reactivated: Configurație IDP reactivată + oidc: + config: + added: Configurație OIDC IDP adăugată + changed: Configurație OIDC IDP schimbată + saml: + config: + added: Configurație SAML IDP adăugată + changed: Configurație SAML IDP schimbată + jwt: + config: + added: Configurație JWT IDP adăugată + changed: Configurație JWT IDP schimbată + customtext: + set: Text personalizat setat + removed: Text personalizat șters + template: + removed: Șablon de text personalizat șters + policy: + login: + added: Politică de conectare adăugată + changed: Politică de conectare schimbată + removed: Politică de conectare ștearsă + idpprovider: + added: Furnizor de identitate adăugat la politica de conectare + removed: Furnizor de identitate șters din politica de conectare + cascade: + removed: Furnizor de identitate cascadă șters din politica de conectare + secondfactor: + added: Al doilea factor adăugat la politica de conectare + removed: Al doilea factor șters din politica de conectare + multifactor: + added: Multi-factor adăugat la politica de conectare + removed: Multi-factor șters din politica de conectare + password: + complexity: + added: Politică de complexitate a parolei adăugată + changed: Politică de complexitate a parolei schimbată + removed: Politică de complexitate a parolei ștearsă + age: + added: Politică de vârstă a parolei adăugată + changed: Politică de vârstă a parolei schimbată + removed: Politică de vârstă a parolei ștearsă + lockout: + added: Politică de blocare a parolei adăugată + changed: Politică de blocare a parolei schimbată + removed: Politică de blocare a parolei ștearsă + label: + added: Politică de etichete adăugată + changed: Politică de etichete schimbată + activated: Politică de etichete activată + removed: Politică de etichete ștearsă + logo: + added: Logo adăugat la politica de etichete + removed: Logo șters din politica de etichete + dark: + added: Logo (modul întunecat) adăugat la politica de etichete + removed: Logo (modul întunecat) șters din politica de etichete + icon: + added: Icon adăugat la politica de etichete + removed: Icon șters din politica de etichete + dark: + added: Icon (modul întunecat) adăugat la politica de etichete + removed: Icon (modul întunecat) șters din politica de etichete + font: + added: Font adăugat la politica de etichete + removed: Font șters din politica de etichete + assets: + removed: Active șterse din politica de etichete + privacy: + added: Politică de confidențialitate și TOS adăugate + changed: Politică de confidențialitate și TOS schimbate + removed: Politică de confidențialitate și TOS șterse + domain: + added: Politică de domeniu adăugată + changed: Politică de domeniu schimbată + removed: Politică de domeniu ștearsă + lockout: + added: Politică de blocare adăugată + changed: Politică de blocare schimbată + removed: Politică de blocare ștearsă + notification: + added: Politică de notificare adăugată + changed: Politică de notificare schimbată + removed: Politică de notificare ștearsă + flow: + trigger_actions: + set: Acțiune setată + cascade: + removed: Acțiuni cascadă șterse + removed: Acțiuni șterse + cleared: Flux șters + mail: + template: + added: Șablon de e-mail adăugat + changed: Șablon de e-mail schimbat + removed: Șablon de e-mail șters + text: + added: Text de e-mail adăugat + changed: Text de e-mail schimbat + removed: Text de e-mail șters + metadata: + removed: Metadate șterse + removed.all: Toate metadatele au fost șterse + set: Metadate setate + project: + added: Proiect adăugat + changed: Proiect schimbat + deactivated: Proiect dezactivat + reactivated: Proiect reactivat + removed: Proiect șters + member: + added: Membru al proiectului adăugat + changed: Membru al proiectului schimbat + removed: Membru al proiectului șters + cascade: + removed: Membru al proiectului cascadă șters + role: + added: Rol de proiect adăugat + changed: Rol de proiect schimbat + removed: Rol de proiect șters + grant: + added: Acces de gestionare adăugat + changed: Acces de gestionare schimbat + removed: Acces de gestionare șters + deactivated: Acces de gestionare dezactivat + reactivated: Acces de gestionare reactivat + cascade: + changed: Acces de gestionare schimbat + member: + added: Membru de acces de gestionare adăugat + changed: Membru de acces de gestionare schimbat + removed: Membru de acces de gestionare șters + cascade: + removed: Acces de gestionare cascadă șters + application: + added: Aplicație adăugată + changed: Aplicație schimbată + removed: Aplicație ștearsă + deactivated: Aplicație dezactivată + reactivated: Aplicație reactivată + oidc: + secret: + check: + succeeded: Verificarea secretului clientului OIDC a reușit + failed: Verificarea secretului clientului OIDC a eșuat + key: + added: Cheie de aplicație OIDC adăugată + removed: Cheie de aplicație OIDC ștearsă + api: + secret: + check: + succeeded: Verificarea secretului API a reușit + failed: Verificarea secretului API a eșuat + key: + added: Cheie de aplicație adăugată + removed: Cheie de aplicație ștearsă + config: + saml: + added: Configurație SAML adăugată + changed: Configurație SAML schimbată + oidc: + added: Configurație OIDC adăugată + changed: Configurație OIDC schimbată + secret: + changed: Secret OIDC schimbat + updated: Hash secret OIDC actualizat + api: + added: Configurație API adăugată + changed: Configurație API schimbată + secret: + changed: Secret API schimbat + updated: Hash secret API actualizat + policy: + password: + complexity: + added: Politică de complexitate a parolei adăugată + changed: Politică de complexitate a parolei schimbată + age: + added: Politică de vârstă a parolei adăugată + changed: Politică de vârstă a parolei schimbată + lockout: + added: Politică de blocare a parolei adăugată + changed: Politică de blocare a parolei schimbată + iam: + setup: + started: Configurarea ZITADEL a început + done: Configurarea ZITADEL a fost finalizată + global: + org: + set: Organizație globală setată + project: + iam: + set: Proiect ZITADEL setat + member: + added: Membru ZITADEL adăugat + changed: Membru ZITADEL schimbat + removed: Membru ZITADEL șters + cascade: + removed: Membru ZITADEL cascadă șters + idp: + config: + added: Configurație IDP adăugată + changed: Configurație IDP schimbată + removed: Configurație IDP ștearsă + deactivated: Configurație IDP dezactivată + reactivated: Configurație IDP reactivată + oidc: + config: + added: Configurație OIDC IDP adăugată + changed: Configurație OIDC IDP schimbată + saml: + config: + added: Configurație SAML IDP adăugată + changed: Configurație SAML IDP schimbată + jwt: + config: + added: Configurație JWT adăugată la furnizorul de identitate + changed: Configurație JWT ștearsă din furnizorul de identitate + customtext: + set: Text a fost setat + removed: Text a fost șters + policy: + login: + added: Politică de conectare implicită adăugată + changed: Politică de conectare implicită schimbată + idpprovider: + added: Furnizor de identitate adăugat la politica de conectare implicită + removed: Furnizor de identitate șters din politica de conectare implicită + label: + added: Politică de etichete adăugată + changed: Politică de etichete schimbată + activated: Politică de etichete activată + logo: + added: Logo adăugat la politica de etichete + removed: Logo șters din politica de etichete + dark: + added: Logo (modul întunecat) adăugat la politica de etichete + removed: Logo (modul întunecat) șters din politica de etichete + icon: + added: Icon adăugat la politica de etichete + removed: Icon șters din politica de etichete + dark: + added: Icon (modul întunecat) adăugat la politica de etichete + removed: Icon (modul întunecat) șters din politica de etichete + font: + added: Font adăugat la politica de etichete + removed: Font șters din politica de etichete + assets: + removed: Active șterse din politica de etichete + default: + language: + set: Limba implicită setată + oidc: + settings: + added: Configurație OIDC adăugată + changed: Configurație OIDC schimbată + removed: Configurație OIDC ștearsă + secret: + generator: + added: Generator de secrete adăugat + changed: Generator de secrete schimbat + removed: Generator de secrete șters + smtp: + config: + added: Configurație SMTP adăugată + changed: Configurație SMTP schimbată + activated: Configurație SMTP activată + deactivated: Configurație SMTP dezactivată + removed: Configurație SMTP ștearsă + password: + changed: Secretul configurației SMTP a fost schimbat + sms: + config: + twilio: + added: Furnizor SMS Twilio adăugat + changed: Furnizor SMS Twilio schimbat + token: + changed: Token furnizor SMS Twilio schimbat + removed: Furnizor SMS Twilio șters + activated: Furnizor SMS Twilio activat + deactivated: Furnizor SMS Twilio dezactivat + key_pair: + added: Pereche de chei adăugată + certificate: + added: Certificat adăugat + action: + added: Acțiune adăugată + changed: Acțiune schimbată + deactivated: Acțiune dezactivată + reactivated: Acțiune reactivată + removed: Acțiune ștearsă + instance: + added: Instanță adăugată + changed: Instanță schimbată + customtext: + removed: Text personalizat șters + set: Text personalizat setat + template: + removed: Șablon de text personalizat șters + default: + language: + set: Limba implicită setată + org: + set: Organizația implicită setată + domain: + added: Domeniu adăugat + primary: + set: Domeniu principal setat + removed: Domeniu șters + iam: + console: + set: Aplicația ZITADEL Console setată + project: + set: Proiectul ZITADEL setat + mail: + template: + added: Șablon de e-mail adăugat + changed: Șablon de e-mail schimbat + text: + added: Text de e-mail adăugat + changed: Text de e-mail schimbat + member: + added: Membru al instanței adăugat + changed: Membru al instanței schimbat + removed: Membru al instanței șters + cascade: + removed: Membru al instanței cascadă șters + notification: + provider: + debug: + fileadded: Furnizor de notificări de depanare a fișierelor adăugat + filechanged: Furnizor de notificări de depanare a fișierelor schimbat + fileremoved: Furnizor de notificări de depanare a fișierelor șters + logadded: Furnizor de notificări de depanare a jurnalelor adăugat + logchanged: Furnizor de notificări de depanare a jurnalelor schimbat + logremoved: Furnizor de notificări de depanare a jurnalelor șters + oidc: + settings: + added: Setări OIDC adăugate + changed: Setări OIDC schimbate + policy: + domain: + added: Politică de domeniu adăugată + changed: Politică de domeniu schimbată + label: + activated: Politică de etichete activată + added: Politică de etichete adăugată + assets: + removed: Activ șters din politica de etichete + changed: Politică de etichete schimbată + font: + added: Font adăugat la politica de etichete + removed: Font șters din politica de etichete + icon: + added: Icon adăugat la politica de etichete + removed: Icon șters din politica de etichete + dark: + added: Icon adăugat la politica de etichete întunecate + removed: Icon șters din politica de etichete întunecate + logo: + added: Logo adăugat la politica de etichete + removed: Logo șters din politica de etichete + dark: + added: Logo adăugat la politica de etichete întunecate + removed: Logo șters din politica de etichete întunecate + lockout: + added: Politică de blocare adăugată + changed: Politică de blocare schimbată + login: + added: Politică de conectare adăugată + changed: Politică de conectare schimbată + idpprovider: + added: Furnizor de identitate adăugat la politica de conectare + cascade: + removed: Furnizor de identitate cascadă șters din politica de conectare + removed: Furnizor de identitate șters din politica de conectare + multifactor: + added: Multifactor adăugat la politica de conectare + removed: Multifactor șters din politica de conectare + secondfactor: + added: Al doilea factor adăugat la politica de conectare + removed: Al doilea factor șters din politica de conectare + password: + age: + added: Politică de vârstă a parolei adăugată + changed: Politică de vârstă a parolei schimbată + complexity: + added: Politică de complexitate a parolei adăugată + changed: Politică de complexitate a parolei schimbată + privacy: + added: Politică de confidențialitate adăugată + changed: Politică de confidențialitate schimbată + security: + set: Politică de securitate setată + + removed: Instanță ștearsă + secret: + generator: + added: Generator de secrete adăugat + changed: Generator de secrete schimbat + removed: Generator de secrete șters + sms: + configtwilio: + activated: Configurație Twilio SMS activată + added: Configurație Twilio SMS adăugată + changed: Configurație Twilio SMS schimbată + deactivated: Configurație Twilio SMS dezactivată + removed: Configurație Twilio SMS ștearsă + token: + changed: Token al configurației Twilio SMS schimbat + smtp: + config: + added: Configurație SMTP adăugată + changed: Configurație SMTP schimbată + activated: Configurație SMTP activată + deactivated: Configurație SMTP dezactivată + password: + changed: Parolă a configurației SMTP schimbată + removed: Configurație SMTP ștearsă + user_schema: + created: Schemă de utilizator creată + updated: Schemă de utilizator actualizată + deactivated: Schemă de utilizator dezactivată + reactivated: Schemă de utilizator reactivată + deleted: Schemă de utilizator ștearsă + user: + created: Utilizator creat + updated: Utilizator actualizat + deleted: Utilizator șters + email: + updated: Adresă de e-mail schimbată + verified: Adresă de e-mail verificată + verification: + failed: Verificarea adresei de e-mail a eșuat + code: + added: Cod de verificare a adresei de e-mail generat + sent: Cod de verificare a adresei de e-mail trimis + phone: + updated: Număr de telefon schimbat + verified: Număr de telefon verificat + verification: + failed: Verificarea numărului de telefon a eșuat + code: + added: Cod de număr de telefon generat + sent: Cod de număr de telefon trimis + + web_key: + added: Cheie Web adăugată + activated: Cheie Web activată + deactivated: Cheie Web dezactivată + removed: Cheie Web ștearsă + + Application: + OIDC: + UnsupportedVersion: Versiunea dvs. OIDC nu este suportată + V1: + NotCompliant: Configurația dvs. nu este conformă și diferă de standardul OIDC 1.0. + NoRedirectUris: Trebuie înregistrat cel puțin un URI de redirecționare. + NotAllCombinationsAreAllowed: Configurația este conformă, dar nu toate combinațiile posibile sunt permise. + Code: + RedirectUris: + HttpOnlyForWeb: Tipul de acordare code permite doar URI-uri de redirecționare http pentru tipul de aplicație web. + CustomOnlyForNative: Tipul de acordare code permite doar URI-uri de redirecționare personalizate pentru tipul de aplicație nativă (de exemplu, appname://) + Implicit: + RedirectUris: + CustomNotAllowed: Tipul de acordare implicit nu permite URI-uri de redirecționare personalizate + HttpNotAllowed: Tipul de acordare implicit nu permite URI-uri de redirecționare http + HttpLocalhostOnlyForNative: URI-ul de redirecționare Http://localhost este permis doar pentru aplicațiile native. + Native: + AuthMethodType: + NotNone: Aplicațiile native ar trebui să aibă authmethodtype none. + RedirectUris: + MustBeHttpLocalhost: URI-urile de redirecționare trebuie să înceapă cu propriul dvs. protocol, http://127.0.0.1, http://[::1] sau http://localhost. + UserAgent: + AuthMethodType: + NotNone: Aplicația agentului utilizator ar trebui să aibă authmethodtype none. + GrantType: + Refresh: + NoAuthCode: Token-ul de reîmprospătare este permis doar în combinație cu codul de autorizare. + + Action: + Flow: + Type: + Unspecified: Nespecificat + ExternalAuthentication: Autentificare externă + CustomiseToken: Completare Token + InternalAuthentication: Autentificare internă + CustomizeSAMLResponse: Completează SAMLResponse + TriggerType: + Unspecified: Nespecificat + PostAuthentication: Post Autentificare + PreCreation: Pre Creare + PostCreation: Post Creare + PreUserinfoCreation: Pre Creare Userinfo + PreAccessTokenCreation: Pre Creare Token de Acces + PreSAMLResponseCreation: Pre Creare Răspuns SAML diff --git a/internal/v2/eventstore/postgres/push.go b/internal/v2/eventstore/postgres/push.go index 09f663a086..bde74687c7 100644 --- a/internal/v2/eventstore/postgres/push.go +++ b/internal/v2/eventstore/postgres/push.go @@ -171,8 +171,7 @@ func (s *Storage) push(ctx context.Context, tx *sql.Tx, reducer eventstore.Reduc cmd.position.InPositionOrder, ) - stmt.WriteString(s.pushPositionStmt) - stmt.WriteString(`)`) + stmt.WriteString(", statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()))") } stmt.WriteString(` RETURNING created_at, "position"`) diff --git a/internal/v2/eventstore/postgres/push_test.go b/internal/v2/eventstore/postgres/push_test.go index 91fdc1fcd7..bb3254427c 100644 --- a/internal/v2/eventstore/postgres/push_test.go +++ b/internal/v2/eventstore/postgres/push_test.go @@ -1288,7 +1288,6 @@ func Test_push(t *testing.T) { }, }, } - initPushStmt("postgres") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dbMock := mock.NewSQLMock(t, append([]mock.Expectation{mock.ExpectBegin(nil)}, tt.args.expectations...)...) @@ -1297,9 +1296,7 @@ func Test_push(t *testing.T) { t.Errorf("unexpected error in begin: %v", err) t.FailNow() } - s := Storage{ - pushPositionStmt: initPushStmt("postgres"), - } + s := Storage{} err = s.push(context.Background(), tx, tt.args.reducer, tt.args.commands) tt.want.assertErr(t, err) dbMock.Assert(t) diff --git a/internal/v2/eventstore/postgres/storage.go b/internal/v2/eventstore/postgres/storage.go index 3a703a7d17..d4148f4f1a 100644 --- a/internal/v2/eventstore/postgres/storage.go +++ b/internal/v2/eventstore/postgres/storage.go @@ -3,8 +3,6 @@ package postgres import ( "context" - "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/v2/eventstore" ) @@ -15,9 +13,8 @@ var ( ) type Storage struct { - client *database.DB - config *Config - pushPositionStmt string + client *database.DB + config *Config } type Config struct { @@ -25,23 +22,9 @@ type Config struct { } func New(client *database.DB, config *Config) *Storage { - initPushStmt(client.Type()) return &Storage{ - client: client, - config: config, - pushPositionStmt: initPushStmt(client.Type()), - } -} - -func initPushStmt(typ string) string { - switch typ { - case "cockroach": - return ", hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp()" - case "postgres": - return ", statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp())" - default: - logging.WithFields("database_type", typ).Panic("position statement for type not implemented") - return "" + client: client, + config: config, } } diff --git a/proto/buf.yaml b/proto/buf.yaml index f8cf192a95..31bc7b4ccc 100644 --- a/proto/buf.yaml +++ b/proto/buf.yaml @@ -7,6 +7,10 @@ deps: breaking: use: - FILE + - FIELD_NO_DELETE_UNLESS_NAME_RESERVED + - FIELD_NO_DELETE_UNLESS_NUMBER_RESERVED + except: + - FIELD_NO_DELETE ignore_unstable_packages: true lint: use: diff --git a/proto/zitadel/action/v2beta/action_service.proto b/proto/zitadel/action/v2beta/action_service.proto new file mode 100644 index 0000000000..f225905225 --- /dev/null +++ b/proto/zitadel/action/v2beta/action_service.proto @@ -0,0 +1,725 @@ +syntax = "proto3"; + +package zitadel.action.v2beta; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +import "zitadel/action/v2beta/target.proto"; +import "zitadel/action/v2beta/execution.proto"; +import "zitadel/action/v2beta/query.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2beta;action"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Action Service"; + version: "2.0-beta"; + description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance. This service is in beta state. It can AND will continue breaking until a stable version is released."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// Service to manage custom executions. +// The service provides methods to create, update, delete and list targets and executions. +service ActionService { + + // Create Target + // + // Create a new target to your endpoint, which can be used in executions. + // + // Required permission: + // - `action.target.write` + // + // Required feature flag: + // - `actions` + rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) { + option (google.api.http) = { + post: "/v2beta/actions/targets" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The target to create already exists."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // Update Target + // + // Update an existing target. + // To generate a new signing key set the optional expirationSigningKey. + // + // Required permission: + // - `action.target.write` + // + // Required feature flag: + // - `actions` + rpc UpdateTarget (UpdateTargetRequest) returns (UpdateTargetResponse) { + option (google.api.http) = { + post: "/v2beta/actions/targets/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target successfully updated or left unchanged"; + }; + }; + responses: { + key: "404" + value: { + description: "The target to update does not exist."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // Delete Target + // + // Delete an existing target. This will remove it from any configured execution as well. + // In case the target is not found, the request will return a successful response as + // the desired state is already achieved. + // + // Required permission: + // - `action.target.delete` + // + // Required feature flag: + // - `actions` + rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) { + option (google.api.http) = { + delete: "/v2beta/actions/targets/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.delete" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target deleted successfully"; + }; + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // Get Target + // + // Returns the target identified by the requested ID. + // + // Required permission: + // - `action.target.read` + // + // Required feature flag: + // - `actions` + rpc GetTarget (GetTargetRequest) returns (GetTargetResponse) { + option (google.api.http) = { + get: "/v2beta/actions/targets/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Target retrieved successfully"; + } + }; + responses: { + key: "404" + value: { + description: "The target to update does not exist."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // List targets + // + // List all matching targets. By default all targets of the instance are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - `action.target.read` + // + // Required feature flag: + // - `actions` + rpc ListTargets (ListTargetsRequest) returns (ListTargetsResponse) { + option (google.api.http) = { + post: "/v2beta/actions/targets/_search", + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all targets matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // Set Execution + // + // Sets an execution to call a target or include the targets of another execution. + // Setting an empty list of targets will remove all targets from the execution, making it a noop. + // + // Required permission: + // - `action.execution.write` + // + // Required feature flag: + // - `actions` + rpc SetExecution (SetExecutionRequest) returns (SetExecutionResponse) { + option (google.api.http) = { + put: "/v2beta/actions/executions" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.execution.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Execution successfully updated or left unchanged"; + }; + }; + responses: { + key: "400" + value: { + description: "Condition to set execution does not exist or the feature flag `actions` is not enabled."; + } + }; + }; + } + + // List Executions + // + // List all matching executions. By default all executions of the instance are returned that have at least one execution target. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - `action.execution.read` + // + // Required feature flag: + // - `actions` + rpc ListExecutions (ListExecutionsRequest) returns (ListExecutionsResponse) { + option (google.api.http) = { + post: "/v2beta/actions/executions/_search" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.execution.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all non noop executions matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "Invalid list query or the feature flag `actions` is not enabled."; + }; + }; + }; + } + + // List Execution Functions + // + // List all available functions which can be used as condition for executions. + rpc ListExecutionFunctions (ListExecutionFunctionsRequest) returns (ListExecutionFunctionsResponse) { + option (google.api.http) = { + get: "/v2beta/actions/executions/functions" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "List all functions successfully"; + }; + }; + }; + } + + // List Execution Methods + // + // List all available methods which can be used as condition for executions. + rpc ListExecutionMethods (ListExecutionMethodsRequest) returns (ListExecutionMethodsResponse) { + option (google.api.http) = { + get: "/v2beta/actions/executions/methods" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "List all methods successfully"; + }; + }; + }; + } + + // List Execution Services + // + // List all available services which can be used as condition for executions. + rpc ListExecutionServices (ListExecutionServicesRequest) returns (ListExecutionServicesResponse) { + option (google.api.http) = { + get: "/v2beta/actions/executions/services" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "List all services successfully"; + }; + }; + }; + } +} + +message CreateTargetRequest { + string name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + min_length: 1 + max_length: 1000 + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + option (validate.required) = true; + // Wait for response but response body is ignored, status is checked, call is sent as post. + RESTWebhook rest_webhook = 2; + // Wait for response and response body is used, status is checked, call is sent as post. + RESTCall rest_call = 3; + // Call is executed in parallel to others, ZITADEL does not wait until the call is finished. The state is ignored, call is sent as post. + RESTAsync rest_async = 4; + } + // Timeout defines the duration until ZITADEL cancels the execution. + // If the target doesn't respond before this timeout expires, then the connection is closed and the action fails. Depending on the target type and possible setting on `interrupt_on_error` following targets will not be called. In case of a `rest_async` target only this specific target will fail, without any influence on other targets of the same execution. + google.protobuf.Duration timeout = 5 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 270}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + string endpoint = 6 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\"" + min_length: 1 + max_length: 1000 + } + ]; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"name\": \"ip_allow_list\",\"restWebhook\":{\"interruptOnError\":true},\"timeout\":\"10s\",\"endpoint\":\"https://example.com/hooks/ip_check\"}"; + }; +} + +message CreateTargetResponse { + // The unique identifier of the newly created target. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the target creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // Key used to sign and check payload sent to the target. + string signing_key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; +} + +message UpdateTargetRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + optional string name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\"" + min_length: 1 + max_length: 1000 + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + // Wait for response but response body is ignored, status is checked, call is sent as post. + RESTWebhook rest_webhook = 3; + // Wait for response and response body is used, status is checked, call is sent as post. + RESTCall rest_call = 4; + // Call is executed in parallel to others, ZITADEL does not wait until the call is finished. The state is ignored, call is sent as post. + RESTAsync rest_async = 5; + } + // Timeout defines the duration until ZITADEL cancels the execution. + // If the target doesn't respond before this timeout expires, then the connection is closed and the action fails. Depending on the target type and possible setting on `interrupt_on_error` following targets will not be called. In case of a `rest_async` target only this specific target will fail, without any influence on other targets of the same execution. + optional google.protobuf.Duration timeout = 6 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 270}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + optional string endpoint = 7 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\"" + min_length: 1 + max_length: 1000 + } + ]; + // Regenerate the key used for signing and checking the payload sent to the target. + // Set the graceful period for the existing key. During that time, the previous + // signing key and the new one will be used to sign the request to allow you a smooth + // transition onf your API. + // + // Note that we currently only allow an immediate rotation ("0s") and will support + // longer expirations in the future. + optional google.protobuf.Duration expiration_signing_key = 8 [ + (validate.rules).duration = {const: {seconds: 0, nanos: 0}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"0s\"" + minimum: 0 + maximum: 0 + } + ]; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"name\": \"ip_allow_list\",\"restCall\":{\"interruptOnError\":true},\"timeout\":\"10s\",\"endpoint\":\"https://example.com/hooks/ip_check\",\"expirationSigningKey\":\"0s\"}"; + }; +} + +message UpdateTargetResponse { + // The timestamp of the change of the target. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // Key used to sign and check payload sent to the target. + optional string signing_key = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; +} + +message DeleteTargetRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeleteTargetResponse { + // The timestamp of the deletion of the target. + // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. + // In case the deletion occurred in a previous request, the deletion date might be empty. + google.protobuf.Timestamp deletion_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GetTargetRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message GetTargetResponse { + Target target = 1; +} + +message ListTargetsRequest { + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional TargetFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"TARGET_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated TargetSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"TARGET_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"targetNameFilter\":{\"targetName\":\"ip_allow_list\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inTargetIdsFilter\":{\"targetIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}"; + }; +} + +message ListTargetsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated Target result = 2; +} + +message SetExecutionRequest { + // Condition defining when the execution should be used. + Condition condition = 1; + // Ordered list of targets called during the execution. + repeated string targets = 2; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"condition\":{\"request\":{\"method\":\"zitadel.session.v2.SessionService/ListSessions\"}},\"targets\":[{\"target\":\"69629026806489455\"}]}"; + }; +} + +message SetExecutionResponse { + // The timestamp of the execution set. + google.protobuf.Timestamp set_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ListExecutionsRequest { + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional ExecutionFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"EXECUTION_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ExecutionSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"EXECUTION_FIELD_NAME_ID\",\"filters\":[{\"targetFilter\":{\"targetId\":\"69629023906488334\"}}]}"; + }; +} + +message ListExecutionsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated Execution result = 2; +} + +message ListExecutionFunctionsRequest{} +message ListExecutionFunctionsResponse{ + // All available methods + repeated string functions = 1; +} +message ListExecutionMethodsRequest{} +message ListExecutionMethodsResponse{ + // All available methods + repeated string methods = 1; +} + +message ListExecutionServicesRequest{} +message ListExecutionServicesResponse{ + // All available methods + repeated string services = 1; +} diff --git a/proto/zitadel/resources/action/v3alpha/execution.proto b/proto/zitadel/action/v2beta/execution.proto similarity index 84% rename from proto/zitadel/resources/action/v3alpha/execution.proto rename to proto/zitadel/action/v2beta/execution.proto index 375ab02b86..e93470e5dc 100644 --- a/proto/zitadel/resources/action/v3alpha/execution.proto +++ b/proto/zitadel/action/v2beta/execution.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package zitadel.resources.action.v3alpha; +package zitadel.action.v2beta; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; @@ -10,31 +10,27 @@ import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; -import "zitadel/resources/object/v3alpha/object.proto"; import "google/protobuf/timestamp.proto"; import "zitadel/object/v3alpha/object.proto"; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2beta;action"; message Execution { - // Ordered list of targets/includes called during the execution. - repeated ExecutionTargetType targets = 1; -} - -message GetExecution { - zitadel.resources.object.v3alpha.Details details = 1; - Condition condition = 2; - Execution execution = 3; -} - -message ExecutionTargetType { - oneof type { - option (validate.required) = true; - // Unique identifier of existing target to call. - string target = 1; - // Unique identifier of existing execution to include targets of. - Condition include = 2; - } + Condition condition = 1; + // The timestamp of the execution creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the execution. + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // Ordered list of targets called during the execution. + repeated string targets = 4; } message Condition { diff --git a/proto/zitadel/resources/action/v3alpha/query.proto b/proto/zitadel/action/v2beta/query.proto similarity index 80% rename from proto/zitadel/resources/action/v3alpha/query.proto rename to proto/zitadel/action/v2beta/query.proto index fb51543085..fe4f72f294 100644 --- a/proto/zitadel/resources/action/v3alpha/query.proto +++ b/proto/zitadel/action/v2beta/query.proto @@ -1,15 +1,16 @@ syntax = "proto3"; -package zitadel.resources.action.v3alpha; +package zitadel.action.v2beta; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2beta;action"; import "google/api/field_behavior.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; -import "zitadel/resources/object/v3alpha/object.proto"; -import "zitadel/resources/action/v3alpha/execution.proto"; +import "zitadel/action/v2beta/execution.proto"; +import "zitadel/filter/v2beta/filter.proto"; message ExecutionSearchFilter { oneof filter { @@ -18,7 +19,6 @@ message ExecutionSearchFilter { InConditionsFilter in_conditions_filter = 1; ExecutionTypeFilter execution_type_filter = 2; TargetFilter target_filter = 3; - IncludeFilter include_filter = 4; } } @@ -42,14 +42,16 @@ message TargetFilter { ]; } -message IncludeFilter { - // Defines the include to query for. - Condition include = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the id of the include" - example: "\"request.zitadel.session.v2.SessionService\""; - } - ]; +enum TargetFieldName { + TARGET_FIELD_NAME_UNSPECIFIED = 0; + TARGET_FIELD_NAME_ID = 1; + TARGET_FIELD_NAME_CREATED_DATE = 2; + TARGET_FIELD_NAME_CHANGED_DATE = 3; + TARGET_FIELD_NAME_NAME = 4; + TARGET_FIELD_NAME_TARGET_TYPE = 5; + TARGET_FIELD_NAME_URL = 6; + TARGET_FIELD_NAME_TIMEOUT = 7; + TARGET_FIELD_NAME_INTERRUPT_ON_ERROR = 8; } message TargetSearchFilter { @@ -71,7 +73,7 @@ message TargetNameFilter { } ]; // Defines which text comparison method used for the name query. - zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ + zitadel.filter.v2beta.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "defines which text equality method is used"; @@ -97,21 +99,10 @@ enum ExecutionType { EXECUTION_TYPE_FUNCTION = 4; } -enum TargetFieldName { - TARGET_FIELD_NAME_UNSPECIFIED = 0; - TARGET_FIELD_NAME_ID = 1; - TARGET_FIELD_NAME_CREATED_DATE = 2; - TARGET_FIELD_NAME_CHANGED_DATE = 3; - TARGET_FIELD_NAME_NAME = 4; - TARGET_FIELD_NAME_TARGET_TYPE = 5; - TARGET_FIELD_NAME_URL = 6; - TARGET_FIELD_NAME_TIMEOUT = 7; - TARGET_FIELD_NAME_INTERRUPT_ON_ERROR = 8; -} enum ExecutionFieldName { EXECUTION_FIELD_NAME_UNSPECIFIED = 0; EXECUTION_FIELD_NAME_ID = 1; EXECUTION_FIELD_NAME_CREATED_DATE = 2; EXECUTION_FIELD_NAME_CHANGED_DATE = 3; -} +} \ No newline at end of file diff --git a/proto/zitadel/action/v2beta/target.proto b/proto/zitadel/action/v2beta/target.proto new file mode 100644 index 0000000000..64da7960ea --- /dev/null +++ b/proto/zitadel/action/v2beta/target.proto @@ -0,0 +1,75 @@ +syntax = "proto3"; + +package zitadel.action.v2beta; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2beta;action"; + +message Target { + // The unique identifier of the target. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the target creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the target (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + string name = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + RESTWebhook rest_webhook = 5; + RESTCall rest_call = 6; + RESTAsync rest_async = 7; + } + // Timeout defines the duration until ZITADEL cancels the execution. + // If the target doesn't respond before this timeout expires, the the connection is closed and the action fails. Depending on the target type and possible setting on `interrupt_on_error` following targets will not be called. In case of a `rest_async` target only this specific target will fail, without any influence on other targets of the same execution. + google.protobuf.Duration timeout = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + string endpoint = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\"" + } + ]; + string signing_key = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; +} + +message RESTWebhook { + // Define if any error stops the whole execution. By default the process continues as normal. + bool interrupt_on_error = 1; +} + +message RESTCall { + // Define if any error stops the whole execution. By default the process continues as normal. + bool interrupt_on_error = 1; +} + +message RESTAsync {} diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 5e5da269ac..d9f8bee2c7 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -41,7 +41,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; tags: [ diff --git a/proto/zitadel/feature/v2/feature_service.proto b/proto/zitadel/feature/v2/feature_service.proto index a89a182632..7b330d4f73 100644 --- a/proto/zitadel/feature/v2/feature_service.proto +++ b/proto/zitadel/feature/v2/feature_service.proto @@ -25,7 +25,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; @@ -113,6 +113,12 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { // reserving the proto field number. Such removal is not considered a breaking change. // Setting a removed field will effectively result in a no-op. service FeatureService { + // Set System Features + // + // Configure and set features that apply to the complete system. Only fields present in the request are set or unset. + // + // Required permissions: + // - system.feature.write rpc SetSystemFeatures (SetSystemFeaturesRequest) returns (SetSystemFeaturesResponse) { option (google.api.http) = { put: "/v2/features/system" @@ -126,8 +132,6 @@ service FeatureService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set system level features"; - description: "Configure and set features that apply to the complete system. Only fields present in the request are set or unset." responses: { key: "200" value: { @@ -137,6 +141,12 @@ service FeatureService { }; } + // Reset System Features + // + // Deletes ALL configured features for the system, reverting the behaviors to system defaults. + // + // Required permissions: + // - system.feature.delete rpc ResetSystemFeatures (ResetSystemFeaturesRequest) returns (ResetSystemFeaturesResponse) { option (google.api.http) = { delete: "/v2/features/system" @@ -149,8 +159,6 @@ service FeatureService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Reset system level features"; - description: "Deletes ALL configured features for the system, reverting the behaviors to system defaults." responses: { key: "200" value: { @@ -160,6 +168,12 @@ service FeatureService { }; } + // Get System Features + // + // Returns all configured features for the system. Unset fields mean the feature is the current system default. + // + // Required permissions: + // - none rpc GetSystemFeatures (GetSystemFeaturesRequest) returns (GetSystemFeaturesResponse) { option (google.api.http) = { get: "/v2/features/system" @@ -167,13 +181,11 @@ service FeatureService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "system.feature.read" + permission: "authenticated" } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get system level features"; - description: "Returns all configured features for the system. Unset fields mean the feature is the current system default." responses: { key: "200" value: { @@ -183,6 +195,12 @@ service FeatureService { }; } + // Set Instance Features + // + // Configure and set features that apply to a complete instance. Only fields present in the request are set or unset. + // + // Required permissions: + // - iam.feature.write rpc SetInstanceFeatures (SetInstanceFeaturesRequest) returns (SetInstanceFeaturesResponse) { option (google.api.http) = { put: "/v2/features/instance" @@ -196,8 +214,6 @@ service FeatureService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set instance level features"; - description: "Configure and set features that apply to a complete instance. Only fields present in the request are set or unset." responses: { key: "200" value: { @@ -207,6 +223,12 @@ service FeatureService { }; } + // Reset Instance Features + // + // Deletes ALL configured features for an instance, reverting the behaviors to system defaults. + // + // Required permissions: + // - iam.feature.delete rpc ResetInstanceFeatures (ResetInstanceFeaturesRequest) returns (ResetInstanceFeaturesResponse) { option (google.api.http) = { delete: "/v2/features/instance" @@ -219,8 +241,6 @@ service FeatureService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Reset instance level features"; - description: "Deletes ALL configured features for an instance, reverting the behaviors to system defaults." responses: { key: "200" value: { @@ -230,6 +250,12 @@ service FeatureService { }; } + // Get Instance Features + // + // Returns all configured features for an instance. Unset fields mean the feature is the current system default. + // + // Required permissions: + // - none rpc GetInstanceFeatures (GetInstanceFeaturesRequest) returns (GetInstanceFeaturesResponse) { option (google.api.http) = { get: "/v2/features/instance" @@ -237,13 +263,11 @@ service FeatureService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "iam.feature.read" + permission: "authenticated" } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get instance level features"; - description: "Returns all configured features for an instance. Unset fields mean the feature is the current system default." responses: { key: "200" value: { @@ -253,6 +277,12 @@ service FeatureService { }; } + // Set Organization Features + // + // Configure and set features that apply to a complete instance. Only fields present in the request are set or unset. + // + // Required permissions: + // - org.feature.write rpc SetOrganizationFeatures (SetOrganizationFeaturesRequest) returns (SetOrganizationFeaturesResponse) { option (google.api.http) = { put: "/v2/features/organization/{organization_id}" @@ -266,8 +296,6 @@ service FeatureService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set organization level features"; - description: "Configure and set features that apply to a complete instance. Only fields present in the request are set or unset." responses: { key: "200" value: { @@ -277,6 +305,12 @@ service FeatureService { }; } + // Reset Organization Features + // + // Deletes ALL configured features for an organization, reverting the behaviors to instance defaults. + // + // Required permissions: + // - org.feature.delete rpc ResetOrganizationFeatures (ResetOrganizationFeaturesRequest) returns (ResetOrganizationFeaturesResponse) { option (google.api.http) = { delete: "/v2/features/organization/{organization_id}" @@ -284,13 +318,11 @@ service FeatureService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.feature.write" + permission: "org.feature.delete" } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Reset organization level features"; - description: "Deletes ALL configured features for an organization, reverting the behaviors to instance defaults." responses: { key: "200" value: { @@ -300,6 +332,13 @@ service FeatureService { }; } + // Get Organization Features + // + // Returns all configured features for an organization. Unset fields mean the feature is the current instance default. + // + // Required permissions: + // - org.feature.read + // - no permission required for the organization the user belongs to rpc GetOrganizationFeatures(GetOrganizationFeaturesRequest) returns (GetOrganizationFeaturesResponse) { option (google.api.http) = { get: "/v2/features/organization/{organization_id}" @@ -307,13 +346,11 @@ service FeatureService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.feature.read" + permission: "authenticated" } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get organization level features"; - description: "Returns all configured features for an organization. Unset fields mean the feature is the current instance default." responses: { key: "200" value: { @@ -323,6 +360,12 @@ service FeatureService { }; } + // Set User Features + // + // Configure and set features that apply to an user. Only fields present in the request are set or unset. + // + // Required permissions: + // - user.feature.write rpc SetUserFeatures(SetUserFeatureRequest) returns (SetUserFeaturesResponse) { option (google.api.http) = { put: "/v2/features/user/{user_id}" @@ -336,8 +379,6 @@ service FeatureService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set user level features"; - description: "Configure and set features that apply to an user. Only fields present in the request are set or unset." responses: { key: "200" value: { @@ -347,6 +388,12 @@ service FeatureService { }; } + // Reset User Features + // + // Deletes ALL configured features for a user, reverting the behaviors to organization defaults. + // + // Required permissions: + // - user.feature.delete rpc ResetUserFeatures(ResetUserFeaturesRequest) returns (ResetUserFeaturesResponse) { option (google.api.http) = { delete: "/v2/features/user/{user_id}" @@ -354,13 +401,11 @@ service FeatureService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "user.feature.write" + permission: "user.feature.delete" } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Reset user level features"; - description: "Deletes ALL configured features for a user, reverting the behaviors to organization defaults." responses: { key: "200" value: { @@ -370,6 +415,13 @@ service FeatureService { }; } + // Get User Features + // + // Returns all configured features for a user. Unset fields mean the feature is the current organization default. + // + // Required permissions: + // - user.feature.read + // - no permission required for the own user rpc GetUserFeatures(GetUserFeaturesRequest) returns (GetUserFeaturesResponse) { option (google.api.http) = { get: "/v2/features/user/{user_id}" @@ -377,13 +429,11 @@ service FeatureService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "user.feature.read" + permission: "authenticated" } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get organization level features"; - description: "Returns all configured features for an organization. Unset fields mean the feature is the current instance default." responses: { key: "200" value: { diff --git a/proto/zitadel/feature/v2/instance.proto b/proto/zitadel/feature/v2/instance.proto index efd7f83e4c..fe8d3f7a39 100644 --- a/proto/zitadel/feature/v2/instance.proto +++ b/proto/zitadel/feature/v2/instance.proto @@ -11,6 +11,8 @@ import "zitadel/feature/v2/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; message SetInstanceFeaturesRequest{ + reserved 6; + reserved "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -43,12 +45,6 @@ message SetInstanceFeaturesRequest{ description: "Enable the experimental `urn:ietf:params:oauth:grant-type:token-exchange` grant type for the OIDC token endpoint. Token exchange can be used to request tokens with a lesser scope or impersonate other users. See the security policy to allow impersonation on an instance."; } ]; - optional bool actions = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; repeated ImprovedPerformance improved_performance = 7 [ (validate.rules).repeated.unique = true, @@ -135,6 +131,8 @@ message GetInstanceFeaturesRequest { } message GetInstanceFeaturesResponse { + reserved 7; + reserved "actions"; zitadel.object.v2.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -171,13 +169,6 @@ message GetInstanceFeaturesResponse { } ]; - FeatureFlag actions = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - ImprovedPerformanceFeatureFlag improved_performance = 8 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[1]"; diff --git a/proto/zitadel/feature/v2/system.proto b/proto/zitadel/feature/v2/system.proto index c734905fb2..d222e2a90c 100644 --- a/proto/zitadel/feature/v2/system.proto +++ b/proto/zitadel/feature/v2/system.proto @@ -11,6 +11,8 @@ import "zitadel/feature/v2/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; message SetSystemFeaturesRequest{ + reserved 6; + reserved "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -46,13 +48,6 @@ message SetSystemFeaturesRequest{ } ]; - optional bool actions = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - repeated ImprovedPerformance improved_performance = 7 [ (validate.rules).repeated.unique = true, (validate.rules).repeated.items.enum = {defined_only: true, not_in: [0]}, @@ -110,6 +105,8 @@ message ResetSystemFeaturesResponse { message GetSystemFeaturesRequest {} message GetSystemFeaturesResponse { + reserved 7; + reserved "actions"; zitadel.object.v2.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -146,13 +143,6 @@ message GetSystemFeaturesResponse { } ]; - FeatureFlag actions = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - ImprovedPerformanceFeatureFlag improved_performance = 8 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[1]"; diff --git a/proto/zitadel/feature/v2beta/feature_service.proto b/proto/zitadel/feature/v2beta/feature_service.proto index 3c610e1c13..daa7124e8f 100644 --- a/proto/zitadel/feature/v2beta/feature_service.proto +++ b/proto/zitadel/feature/v2beta/feature_service.proto @@ -25,7 +25,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/feature/v2beta/instance.proto b/proto/zitadel/feature/v2beta/instance.proto index 865a1d2308..7717dd7556 100644 --- a/proto/zitadel/feature/v2beta/instance.proto +++ b/proto/zitadel/feature/v2beta/instance.proto @@ -11,6 +11,8 @@ import "zitadel/feature/v2beta/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature"; message SetInstanceFeaturesRequest{ + reserved 6; + reserved "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -43,12 +45,6 @@ message SetInstanceFeaturesRequest{ description: "Enable the experimental `urn:ietf:params:oauth:grant-type:token-exchange` grant type for the OIDC token endpoint. Token exchange can be used to request tokens with a lesser scope or impersonate other users. See the security policy to allow impersonation on an instance."; } ]; - optional bool actions = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; repeated ImprovedPerformance improved_performance = 7 [ (validate.rules).repeated.unique = true, @@ -101,6 +97,8 @@ message GetInstanceFeaturesRequest { } message GetInstanceFeaturesResponse { + reserved 7; + reserved "actions"; zitadel.object.v2beta.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -137,13 +135,6 @@ message GetInstanceFeaturesResponse { } ]; - FeatureFlag actions = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - ImprovedPerformanceFeatureFlag improved_performance = 8 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[1]"; diff --git a/proto/zitadel/feature/v2beta/system.proto b/proto/zitadel/feature/v2beta/system.proto index 98b37ad893..624e68ec79 100644 --- a/proto/zitadel/feature/v2beta/system.proto +++ b/proto/zitadel/feature/v2beta/system.proto @@ -11,6 +11,8 @@ import "zitadel/feature/v2beta/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature"; message SetSystemFeaturesRequest{ + reserved 6; + reserved "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -46,13 +48,6 @@ message SetSystemFeaturesRequest{ } ]; - optional bool actions = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - repeated ImprovedPerformance improved_performance = 7 [ (validate.rules).repeated.unique = true, (validate.rules).repeated.items.enum = {defined_only: true, not_in: [0]}, @@ -83,6 +78,8 @@ message ResetSystemFeaturesResponse { message GetSystemFeaturesRequest {} message GetSystemFeaturesResponse { + reserved 7; + reserved "actions"; zitadel.object.v2beta.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -119,13 +116,6 @@ message GetSystemFeaturesResponse { } ]; - FeatureFlag actions = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - ImprovedPerformanceFeatureFlag improved_performance = 8 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[1]"; diff --git a/proto/zitadel/filter/v2beta/filter.proto b/proto/zitadel/filter/v2beta/filter.proto new file mode 100644 index 0000000000..6aae583cde --- /dev/null +++ b/proto/zitadel/filter/v2beta/filter.proto @@ -0,0 +1,59 @@ +syntax = "proto3"; + +package zitadel.filter.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta;filter"; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +enum TextFilterMethod { + TEXT_FILTER_METHOD_EQUALS = 0; + TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE = 1; + TEXT_FILTER_METHOD_STARTS_WITH = 2; + TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE = 3; + TEXT_FILTER_METHOD_CONTAINS = 4; + TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE = 5; + TEXT_FILTER_METHOD_ENDS_WITH = 6; + TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE = 7; +} + +enum ListFilterMethod { + LIST_FILTER_METHOD_IN = 0; +} + +enum TimestampFilterMethod { + TIMESTAMP_FILTER_METHOD_EQUALS = 0; + TIMESTAMP_FILTER_METHOD_GREATER = 1; + TIMESTAMP_FILTER_METHOD_GREATER_OR_EQUALS = 2; + TIMESTAMP_FILTER_METHOD_LESS = 3; + TIMESTAMP_FILTER_METHOD_LESS_OR_EQUALS = 4; +} + +message PaginationRequest { + // Starting point for retrieval, in combination of offset used to query a set list of objects. + uint64 offset = 1; + // limit is the maximum amount of objects returned. The default is set to 100 + // with a maximum of 1000 in the runtime configuration. + // If the limit exceeds the maximum configured ZITADEL will throw an error. + // If no limit is present the default is taken. + uint32 limit = 2; + // Asc is the sorting order. If true the list is sorted ascending, if false + // the list is sorted descending. The default is descending. + bool asc = 3; +} + +message PaginationResponse { + // Absolute number of objects matching the query, regardless of applied limit. + uint64 total_result = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"100\""; + } + ]; + // Applied limit from query, defines maximum amount of objects per request, to compare if all objects are returned. + uint64 applied_limit = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"100\""; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/idp/v2/idp_service.proto b/proto/zitadel/idp/v2/idp_service.proto index 2d5306cea6..418704779c 100644 --- a/proto/zitadel/idp/v2/idp_service.proto +++ b/proto/zitadel/idp/v2/idp_service.proto @@ -24,7 +24,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/oidc/v2/oidc_service.proto b/proto/zitadel/oidc/v2/oidc_service.proto index e305cbfe9a..4624910c65 100644 --- a/proto/zitadel/oidc/v2/oidc_service.proto +++ b/proto/zitadel/oidc/v2/oidc_service.proto @@ -24,7 +24,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/oidc/v2beta/oidc_service.proto b/proto/zitadel/oidc/v2beta/oidc_service.proto index d962c90a91..e984e4db51 100644 --- a/proto/zitadel/oidc/v2beta/oidc_service.proto +++ b/proto/zitadel/oidc/v2beta/oidc_service.proto @@ -24,7 +24,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/org/v2/org_service.proto b/proto/zitadel/org/v2/org_service.proto index 3917fc85a6..94ced55146 100644 --- a/proto/zitadel/org/v2/org_service.proto +++ b/proto/zitadel/org/v2/org_service.proto @@ -34,7 +34,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/org/v2beta/org_service.proto b/proto/zitadel/org/v2beta/org_service.proto index 132f0f1f30..90c29ca354 100644 --- a/proto/zitadel/org/v2beta/org_service.proto +++ b/proto/zitadel/org/v2beta/org_service.proto @@ -33,7 +33,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/resources/action/v3alpha/action_service.proto b/proto/zitadel/resources/action/v3alpha/action_service.proto deleted file mode 100644 index bc3739861d..0000000000 --- a/proto/zitadel/resources/action/v3alpha/action_service.proto +++ /dev/null @@ -1,565 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.action.v3alpha; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; - -import "zitadel/protoc_gen_zitadel/v2/options.proto"; - -import "zitadel/resources/action/v3alpha/target.proto"; -import "zitadel/resources/action/v3alpha/execution.proto"; -import "zitadel/resources/action/v3alpha/query.proto"; -import "zitadel/resources/object/v3alpha/object.proto"; -import "zitadel/object/v3alpha/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; - -option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { - info: { - title: "Action Service"; - version: "3.0-alpha"; - description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance. It will continue breaking as long as it is in alpha state."; - contact:{ - name: "ZITADEL" - url: "https://zitadel.com" - email: "hi@zitadel.com" - } - license: { - name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; - }; - }; - schemes: HTTPS; - schemes: HTTP; - - consumes: "application/json"; - consumes: "application/grpc"; - - produces: "application/json"; - produces: "application/grpc"; - - consumes: "application/grpc-web+proto"; - produces: "application/grpc-web+proto"; - - host: "$CUSTOM-DOMAIN"; - base_path: "/"; - - external_docs: { - description: "Detailed information about ZITADEL", - url: "https://zitadel.com/docs" - } - security_definitions: { - security: { - key: "OAuth2"; - value: { - type: TYPE_OAUTH2; - flow: FLOW_ACCESS_CODE; - authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; - token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; - scopes: { - scope: { - key: "openid"; - value: "openid"; - } - scope: { - key: "urn:zitadel:iam:org:project:id:zitadel:aud"; - value: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - } - } - security: { - security_requirement: { - key: "OAuth2"; - value: { - scope: "openid"; - scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - responses: { - key: "403"; - value: { - description: "Returned when the user does not have permission to access the resource."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } - responses: { - key: "404"; - value: { - description: "Returned when the resource does not exist."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } -}; - -service ZITADELActions { - - // Create a target - // - // Create a new target, which can be used in executions. - rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) { - option (google.api.http) = { - post: "/resources/v3alpha/actions/targets" - body: "target" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.write" - } - http_response: { - success_code: 201 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "201"; - value: { - description: "Target successfully created"; - schema: { - json_schema: { - ref: "#/definitions/v3alphaCreateTargetResponse"; - } - } - }; - }; - }; - } - - // Patch a target - // - // Patch an existing target. - rpc PatchTarget (PatchTargetRequest) returns (PatchTargetResponse) { - option (google.api.http) = { - patch: "/resources/v3alpha/actions/targets/{id}" - body: "target" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.write" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Target successfully updated or left unchanged"; - }; - }; - }; - } - - // Delete a target - // - // Delete an existing target. This will remove it from any configured execution as well. - rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) { - option (google.api.http) = { - delete: "/resources/v3alpha/actions/targets/{id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.delete" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Target successfully deleted"; - }; - }; - }; - } - - // Target by ID - // - // Returns the target identified by the requested ID. - rpc GetTarget (GetTargetRequest) returns (GetTargetResponse) { - option (google.api.http) = { - get: "/resources/v3alpha/actions/targets/{id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200" - value: { - description: "Target successfully retrieved"; - } - }; - }; - } - - // Search targets - // - // Search all matching targets. By default all targets of the instance are returned. - // Make sure to include a limit and sorting for pagination. - rpc SearchTargets (SearchTargetsRequest) returns (SearchTargetsResponse) { - option (google.api.http) = { - post: "/resources/v3alpha/actions/targets/_search", - body: "filters" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "A list of all targets matching the query"; - }; - }; - responses: { - key: "400"; - value: { - description: "invalid list query"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; - }; - } - - // Sets an execution to call a target or include the targets of another execution. - // - // Setting an empty list of targets will remove all targets from the execution, making it a noop. - rpc SetExecution (SetExecutionRequest) returns (SetExecutionResponse) { - option (google.api.http) = { - put: "/resources/v3alpha/actions/executions" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.execution.write" - } - http_response: { - success_code: 201 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Execution successfully updated or left unchanged"; - schema: { - json_schema: { - ref: "#/definitions/v3alphaSetExecutionResponse"; - } - } - }; - }; - }; - } - - // Search executions - // - // Search all matching executions. By default all executions of the instance are returned that have at least one execution target. - // Make sure to include a limit and sorting for pagination. - rpc SearchExecutions (SearchExecutionsRequest) returns (SearchExecutionsResponse) { - option (google.api.http) = { - post: "/resources/v3alpha/actions/executions/_search" - body: "filters" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "A list of all non noop executions matching the query"; - }; - }; - responses: { - key: "400"; - value: { - description: "invalid list query"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; - }; - } - - // List all available functions - // - // List all available functions which can be used as condition for executions. - rpc ListExecutionFunctions (ListExecutionFunctionsRequest) returns (ListExecutionFunctionsResponse) { - option (google.api.http) = { - get: "/resources/v3alpha/actions/executions/functions" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "authenticated" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all functions successfully"; - }; - }; - }; - } - // List all available methods - // - // List all available methods which can be used as condition for executions. - rpc ListExecutionMethods (ListExecutionMethodsRequest) returns (ListExecutionMethodsResponse) { - option (google.api.http) = { - get: "/resources/v3alpha/actions/executions/methods" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "authenticated" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all methods successfully"; - }; - }; - }; - } - // List all available service - // - // List all available services which can be used as condition for executions. - rpc ListExecutionServices (ListExecutionServicesRequest) returns (ListExecutionServicesResponse) { - option (google.api.http) = { - get: "/resources/v3alpha/actions/executions/services" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "authenticated" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all services successfully"; - }; - }; - }; - } -} - -message CreateTargetRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - Target target = 2 [ - (validate.rules).message = { - required: true - } - ]; -} - -message CreateTargetResponse { - zitadel.resources.object.v3alpha.Details details = 1; - // Key used to sign and check payload sent to the target. - string signing_key = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"98KmsU67\"" - } - ]; -} - -message PatchTargetRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - string id = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; - PatchTarget target = 3 [ - (validate.rules).message = { - required: true - } - ]; -} - -message PatchTargetResponse { - zitadel.resources.object.v3alpha.Details details = 1; - // Key used to sign and check payload sent to the target. - optional string signing_key = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"98KmsU67\"" - } - ]; -} - -message DeleteTargetRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - string id = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message DeleteTargetResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message GetTargetRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - string id = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message GetTargetResponse { - GetTarget target = 1; -} - -message SearchTargetsRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - // list limitations and ordering. - optional zitadel.resources.object.v3alpha.SearchQuery query = 2; - // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. - optional TargetFieldName sorting_column = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"TARGET_FIELD_NAME_CREATION_DATE\"" - } - ]; - // Define the criteria to query for. - repeated TargetSearchFilter filters = 4; -} - -message SearchTargetsResponse { - zitadel.resources.object.v3alpha.ListDetails details = 1; - repeated GetTarget result = 2; -} - -message SetExecutionRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - Condition condition = 2; - Execution execution = 3; -} - -message SetExecutionResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message SearchExecutionsRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - // list limitations and ordering. - optional zitadel.resources.object.v3alpha.SearchQuery query = 2; - // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. - optional ExecutionFieldName sorting_column = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"EXECUTION_FIELD_NAME_CREATION_DATE\"" - } - ]; - // Define the criteria to query for. - repeated ExecutionSearchFilter filters = 4; -} - -message SearchExecutionsResponse { - zitadel.resources.object.v3alpha.ListDetails details = 1; - repeated GetExecution result = 2; -} - -message ListExecutionFunctionsRequest{} -message ListExecutionFunctionsResponse{ - // All available methods - repeated string functions = 1; -} -message ListExecutionMethodsRequest{} -message ListExecutionMethodsResponse{ - // All available methods - repeated string methods = 1; -} - -message ListExecutionServicesRequest{} -message ListExecutionServicesResponse{ - // All available methods - repeated string services = 1; -} diff --git a/proto/zitadel/resources/action/v3alpha/target.proto b/proto/zitadel/resources/action/v3alpha/target.proto deleted file mode 100644 index 8524ab3639..0000000000 --- a/proto/zitadel/resources/action/v3alpha/target.proto +++ /dev/null @@ -1,124 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.action.v3alpha; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; -import "zitadel/protoc_gen_zitadel/v2/options.proto"; -import "google/protobuf/timestamp.proto"; - -import "zitadel/resources/object/v3alpha/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; - -message Target { - string name = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"ip_allow_list\""; - min_length: 1 - max_length: 1000 - } - ]; - // Defines the target type and how the response of the target is treated. - oneof target_type { - option (validate.required) = true; - SetRESTWebhook rest_webhook = 2; - SetRESTCall rest_call = 3; - SetRESTAsync rest_async = 4; - } - // Timeout defines the duration until ZITADEL cancels the execution. - google.protobuf.Duration timeout = 5 [ - (validate.rules).duration = {gte: {}, lte: {seconds: 270}}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "if the target doesn't respond before this timeout expires, the the connection is closed and the action fails"; - example: "\"10s\""; - } - ]; - string endpoint = 6 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://example.com/hooks/ip_check\"" - min_length: 1 - max_length: 1000 - } - ]; -} - -message GetTarget { - zitadel.resources.object.v3alpha.Details details = 1; - Target config = 2; - string signing_key = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"98KmsU67\"" - } - ]; -} - -message PatchTarget { - optional string name = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"ip_allow_list\"" - min_length: 1 - max_length: 1000 - } - ]; - // Defines the target type and how the response of the target is treated. - oneof target_type { - SetRESTWebhook rest_webhook = 2; - SetRESTCall rest_call = 3; - SetRESTAsync rest_async = 4; - } - // Timeout defines the duration until ZITADEL cancels the execution. - optional google.protobuf.Duration timeout = 5 [ - (validate.rules).duration = {gte: {}, lte: {seconds: 270}}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "if the target doesn't respond before this timeout expires, the the connection is closed and the action fails"; - example: "\"10s\""; - } - ]; - optional string endpoint = 6 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://example.com/hooks/ip_check\"" - min_length: 1 - max_length: 1000 - } - ]; - // Regenerate the key used for signing and checking the payload sent to the target. - // Set the graceful period for the existing key. During that time, the previous - // signing key and the new one will be used to sign the request to allow you a smooth - // transition onf your API. - // - // Note that we currently only allow an immediate rotation ("0s") and will support - // longer expirations in the future. - optional google.protobuf.Duration expiration_signing_key = 7 [ - (validate.rules).duration = {const: {seconds: 0, nanos: 0}}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"0s\"" - minimum: 0 - maximum: 0 - } - ]; -} - - -// Wait for response but response body is ignored, status is checked, call is sent as post. -message SetRESTWebhook { - // Define if any error stops the whole execution. By default the process continues as normal. - bool interrupt_on_error = 1; -} - -// Wait for response and response body is used, status is checked, call is sent as post. -message SetRESTCall { - // Define if any error stops the whole execution. By default the process continues as normal. - bool interrupt_on_error = 1; -} - -// Call is executed in parallel to others, ZITADEL does not wait until the call is finished. The state is ignored, call is sent as post. -message SetRESTAsync {} diff --git a/proto/zitadel/resources/debug_events/v3alpha/debug_events_service.proto b/proto/zitadel/resources/debug_events/v3alpha/debug_events_service.proto index 6a5990f783..c2b0be5226 100644 --- a/proto/zitadel/resources/debug_events/v3alpha/debug_events_service.proto +++ b/proto/zitadel/resources/debug_events/v3alpha/debug_events_service.proto @@ -27,7 +27,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/resources/user/v3alpha/user_service.proto b/proto/zitadel/resources/user/v3alpha/user_service.proto index 4e297d5ed1..2a7e87d923 100644 --- a/proto/zitadel/resources/user/v3alpha/user_service.proto +++ b/proto/zitadel/resources/user/v3alpha/user_service.proto @@ -30,7 +30,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto b/proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto index ae9ef6ec8b..ea68923eec 100644 --- a/proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto +++ b/proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto @@ -27,7 +27,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/resources/webkey/v3alpha/config.proto b/proto/zitadel/resources/webkey/v3alpha/config.proto deleted file mode 100644 index 170334afa5..0000000000 --- a/proto/zitadel/resources/webkey/v3alpha/config.proto +++ /dev/null @@ -1,41 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.webkey.v3alpha; - -import "validate/validate.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey"; - -message WebKeyRSAConfig { - enum RSABits { - RSA_BITS_UNSPECIFIED = 0; - RSA_BITS_2048 = 1; - RSA_BITS_3072 = 2; - RSA_BITS_4096 = 3; - } - - enum RSAHasher { - RSA_HASHER_UNSPECIFIED = 0; - RSA_HASHER_SHA256 = 1; - RSA_HASHER_SHA384 = 2; - RSA_HASHER_SHA512 = 3; - } - - // bit size of the RSA key - RSABits bits = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; - // signing algrithm used - RSAHasher hasher = 2 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; -} - -message WebKeyECDSAConfig { - enum ECDSACurve { - ECDSA_CURVE_UNSPECIFIED = 0; - ECDSA_CURVE_P256 = 1; - ECDSA_CURVE_P384 = 2; - ECDSA_CURVE_P512 = 3; - } - - ECDSACurve curve = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; -} - -message WebKeyED25519Config {} diff --git a/proto/zitadel/resources/webkey/v3alpha/key.proto b/proto/zitadel/resources/webkey/v3alpha/key.proto deleted file mode 100644 index 47486f7aee..0000000000 --- a/proto/zitadel/resources/webkey/v3alpha/key.proto +++ /dev/null @@ -1,31 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.webkey.v3alpha; - -import "google/protobuf/timestamp.proto"; -import "zitadel/resources/webkey/v3alpha/config.proto"; -import "zitadel/resources/object/v3alpha/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey"; - -enum WebKeyState { - STATE_UNSPECIFIED = 0; - STATE_INITIAL = 1; - STATE_ACTIVE = 2; - STATE_INACTIVE = 3; - STATE_REMOVED = 4; -} - -message GetWebKey { - zitadel.resources.object.v3alpha.Details details = 1; - WebKey config = 2; - WebKeyState state = 3; -} - -message WebKey { - oneof config { - WebKeyRSAConfig rsa = 6; - WebKeyECDSAConfig ecdsa = 7; - WebKeyED25519Config ed25519 = 8; - } -} diff --git a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto deleted file mode 100644 index 43d2edab2b..0000000000 --- a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto +++ /dev/null @@ -1,278 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.webkey.v3alpha; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; - -import "zitadel/protoc_gen_zitadel/v2/options.proto"; - -import "zitadel/resources/webkey/v3alpha/key.proto"; -import "zitadel/resources/object/v3alpha/object.proto"; -import "zitadel/object/v3alpha/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey"; - -option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { - info: { - title: "Web key Service"; - version: "3.0-preview"; - description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This project is in preview state. It can AND will continue breaking until a stable version is released."; - contact:{ - name: "ZITADEL" - url: "https://zitadel.com" - email: "hi@zitadel.com" - } - license: { - name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; - }; - }; - schemes: HTTPS; - schemes: HTTP; - - consumes: "application/json"; - produces: "application/json"; - - consumes: "application/grpc"; - produces: "application/grpc"; - - consumes: "application/grpc-web+proto"; - produces: "application/grpc-web+proto"; - - host: "$CUSTOM-DOMAIN"; - base_path: "/"; - - external_docs: { - description: "Detailed information about ZITADEL", - url: "https://zitadel.com/docs" - } - security_definitions: { - security: { - key: "OAuth2"; - value: { - type: TYPE_OAUTH2; - flow: FLOW_ACCESS_CODE; - authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; - token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; - scopes: { - scope: { - key: "openid"; - value: "openid"; - } - scope: { - key: "urn:zitadel:iam:org:project:id:zitadel:aud"; - value: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - } - } - security: { - security_requirement: { - key: "OAuth2"; - value: { - scope: "openid"; - scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - responses: { - key: "403"; - value: { - description: "Returned when the user does not have permission to access the resource."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } - responses: { - key: "404"; - value: { - description: "Returned when the resource does not exist."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } -}; - -service ZITADELWebKeys { - rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) { - option (google.api.http) = { - post: "/resources/v3alpha/web_keys" - body: "key" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "iam.web_key.write" - } - http_response: { - success_code: 201 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Generate a web key pair for the instance"; - description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens." - responses: { - key: "200" - value: { - description: "OK"; - } - }; - }; - } - - rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) { - option (google.api.http) = { - post: "/resources/v3alpha/web_keys/{id}/_activate" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "iam.web_key.write" - } - http_response: { - success_code: 200 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Activate a signing key for the instance"; - description: "Switch the active signing web key. The previously active key will be deactivated. Note that the JWKs OIDC endpoint returns a cacheable response. Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min), as the public key may not have been propagated to caches and clients yet." - responses: { - key: "200" - value: { - description: "OK"; - } - }; - }; - } - - rpc DeleteWebKey(DeleteWebKeyRequest) returns (DeleteWebKeyResponse) { - option (google.api.http) = { - delete: "/resources/v3alpha/web_keys/{id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "iam.web_key.delete" - } - http_response: { - success_code: 200 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete a web key pair for the instance"; - description: "Delete a web key pair. Only inactive keys can be deleted. Once a key is deleted, any tokens signed by this key will be invalid." - responses: { - key: "200" - value: { - description: "OK"; - } - }; - }; - } - - rpc ListWebKeys(ListWebKeysRequest) returns (ListWebKeysResponse) { - option (google.api.http) = { - get: "/resources/v3alpha/web_keys" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "iam.web_key.read" - } - http_response: { - success_code: 200 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "List web key details for the instance"; - description: "List web key details for the instance" - responses: { - key: "200" - value: { - description: "OK"; - } - }; - }; - } -} - -message CreateWebKeyRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - WebKey key = 2; -} - -message CreateWebKeyResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message ActivateWebKeyRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - string id = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message ActivateWebKeyResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message DeleteWebKeyRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - string id = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message DeleteWebKeyResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message ListWebKeysRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; -} - -message ListWebKeysResponse { - repeated GetWebKey web_keys = 1; -} \ No newline at end of file diff --git a/proto/zitadel/saml/v2/saml_service.proto b/proto/zitadel/saml/v2/saml_service.proto index 3198cf3086..c6c39886ad 100644 --- a/proto/zitadel/saml/v2/saml_service.proto +++ b/proto/zitadel/saml/v2/saml_service.proto @@ -24,7 +24,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/session/v2/session_service.proto b/proto/zitadel/session/v2/session_service.proto index 74e20263fe..6b3ef9f2a7 100644 --- a/proto/zitadel/session/v2/session_service.proto +++ b/proto/zitadel/session/v2/session_service.proto @@ -28,7 +28,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/session/v2beta/session_service.proto b/proto/zitadel/session/v2beta/session_service.proto index 6a2e731a94..554d636203 100644 --- a/proto/zitadel/session/v2beta/session_service.proto +++ b/proto/zitadel/session/v2beta/session_service.proto @@ -28,7 +28,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/settings/v2/settings_service.proto b/proto/zitadel/settings/v2/settings_service.proto index 77c20eb1c6..7f71e08da4 100644 --- a/proto/zitadel/settings/v2/settings_service.proto +++ b/proto/zitadel/settings/v2/settings_service.proto @@ -30,7 +30,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/settings/v2beta/settings_service.proto b/proto/zitadel/settings/v2beta/settings_service.proto index 88331ddc54..9404e002a7 100644 --- a/proto/zitadel/settings/v2beta/settings_service.proto +++ b/proto/zitadel/settings/v2beta/settings_service.proto @@ -30,7 +30,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index b5852b1fec..f124c37a79 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -30,7 +30,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; tags: [ diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 5457efd64e..00cb352f70 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -32,7 +32,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; @@ -2094,6 +2094,7 @@ message RetrieveIdentityProviderIntentResponse{ example: "\"163840776835432345\""; } ]; + AddHumanUserRequest add_human_user = 4; } message AddIDPLinkRequest{ diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto index 9ad0a7e6eb..03bc36220e 100644 --- a/proto/zitadel/user/v2beta/user_service.proto +++ b/proto/zitadel/user/v2beta/user_service.proto @@ -32,7 +32,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/webkey/v2beta/key.proto b/proto/zitadel/webkey/v2beta/key.proto new file mode 100644 index 0000000000..b2a8a380b2 --- /dev/null +++ b/proto/zitadel/webkey/v2beta/key.proto @@ -0,0 +1,109 @@ +syntax = "proto3"; + +package zitadel.webkey.v2beta; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta;webkey"; + +enum State { + STATE_UNSPECIFIED = 0; + // A newly created key is in the initial state and published to the public key endpoint. + STATE_INITIAL = 1; + // The active key is used to sign tokens. Only one key can be active at a time. + STATE_ACTIVE = 2; + // The inactive key is not used to sign tokens anymore, but still published to the public key endpoint. + STATE_INACTIVE = 3; + // The removed key is not used to sign tokens anymore and not published to the public key endpoint. + STATE_REMOVED = 4; +} + +message WebKey { + // The unique identifier of the key. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the key (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // State of the key + State state = 4; + // Configured type of the key (either RSA, ECDSA or ED25519) + oneof key { + RSA rsa = 5; + ECDSA ecdsa = 6; + ED25519 ed25519 = 7; + } +} + +message RSA { + // Bit size of the RSA key. Default is 2048 bits. + RSABits bits = 1 [ + (validate.rules).enum = {defined_only: true, not_in: [0]}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "RSA_BITS_2048"; + } + ]; + // Signing algrithm used. Default is SHA256. + RSAHasher hasher = 2 [ + (validate.rules).enum = {defined_only: true, not_in: [0]}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "RSA_HASHER_SHA256"; + } + ]; +} + +enum RSABits { + RSA_BITS_UNSPECIFIED = 0; + // 2048 bit RSA key + RSA_BITS_2048 = 1; + // 3072 bit RSA key + RSA_BITS_3072 = 2; + // 4096 bit RSA key + RSA_BITS_4096 = 3; +} + +enum RSAHasher { + RSA_HASHER_UNSPECIFIED = 0; + // SHA256 hashing algorithm resulting in the RS256 algorithm header + RSA_HASHER_SHA256 = 1; + // SHA384 hashing algorithm resulting in the RS384 algorithm header + RSA_HASHER_SHA384 = 2; + // SHA512 hashing algorithm resulting in the RS512 algorithm header + RSA_HASHER_SHA512 = 3; +} + +message ECDSA { + // Curve of the ECDSA key. Default is P-256. + ECDSACurve curve = 1 [ + (validate.rules).enum = {defined_only: true, not_in: [0]}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "ECDSA_CURVE_P256"; + } + ]; +} + +enum ECDSACurve { + ECDSA_CURVE_UNSPECIFIED = 0; + // NIST P-256 curve resulting in the ES256 algorithm header + ECDSA_CURVE_P256 = 1; + // NIST P-384 curve resulting in the ES384 algorithm header + ECDSA_CURVE_P384 = 2; + // NIST P-512 curve resulting in the ES512 algorithm header + ECDSA_CURVE_P512 = 3; +} + +message ED25519 {} diff --git a/proto/zitadel/webkey/v2beta/webkey_service.proto b/proto/zitadel/webkey/v2beta/webkey_service.proto new file mode 100644 index 0000000000..ca39be5e1c --- /dev/null +++ b/proto/zitadel/webkey/v2beta/webkey_service.proto @@ -0,0 +1,359 @@ +syntax = "proto3"; + +package zitadel.webkey.v2beta; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/webkey/v2beta/key.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta;webkey"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Web key Service"; + version: "2.0-beta"; + description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This service is in beta state. It can AND will continue breaking until a stable version is released.\n\nThe public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.\n\nPlease make sure to enable the `web_key` feature flag on your instance to use this service."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + produces: "application/json"; + + consumes: "application/grpc"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// Service to manage web keys for OIDC token signing and validation. +// The service provides methods to create, activate, delete and list web keys. +// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys. +// +// Please make sure to enable the `web_key` feature flag on your instance to use this service. +service WebKeyService { + // Create Web Key + // + // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. + // The public key can be used to validate OIDC tokens. + // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint. + // Note that the JWKs OIDC endpoint returns a cacheable response. + // + // If no key type is provided, a RSA key pair with 2048 bits and SHA256 hashing will be created. + // + // Required permission: + // - `iam.web_key.write` + // + // Required feature flag: + // - `web_key` + rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) { + option (google.api.http) = { + post: "/v2beta/web_keys" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Web key created successfully."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `web_key` is not enabled."; + } + }; + }; + } + + // Activate Web Key + // + // Switch the active signing web key. The previously active key will be deactivated. + // Note that the JWKs OIDC endpoint returns a cacheable response. + // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min), + // as the public key may not have been propagated to caches and clients yet. + // + // Required permission: + // - `iam.web_key.write` + // + // Required feature flag: + // - `web_key` + rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) { + option (google.api.http) = { + post: "/v2beta/web_keys/{id}/activate" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Web key activated successfully."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `web_key` is not enabled."; + } + }; + responses: { + key: "404" + value: { + description: "The web key to active does not exist."; + } + }; + }; + } + + // Delete Web Key + // + // Delete a web key pair. Only inactive keys can be deleted. Once a key is deleted, + // any tokens signed by this key will be invalid. + // Note that the JWKs OIDC endpoint returns a cacheable response. + // In case the web key is not found, the request will return a successful response as + // the desired state is already achieved. + // You can check the change date in the response to verify if the web key was deleted during the request. + // + // Required permission: + // - `iam.web_key.delete` + // + // Required feature flag: + // - `web_key` + rpc DeleteWebKey(DeleteWebKeyRequest) returns (DeleteWebKeyResponse) { + option (google.api.http) = { + delete: "/v2beta/web_keys/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.delete" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Web key deleted successfully."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `web_key` is not enabled or the web key is currently active."; + } + }; + }; + } + + // List Web Keys + // + // List all web keys and their states. + // + // Required permission: + // - `iam.web_key.read` + // + // Required feature flag: + // - `web_key` + rpc ListWebKeys(ListWebKeysRequest) returns (ListWebKeysResponse) { + option (google.api.http) = { + get: "/v2beta/web_keys" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "List of all web keys."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `web_key` is not enabled."; + } + }; + }; + } +} + +message CreateWebKeyRequest { + // The key type to create (RSA, ECDSA, ED25519). + // If no key type is provided, a RSA key pair with 2048 bits and SHA256 hashing will be created. + oneof key { + // Create a RSA key pair and specify the bit size and hashing algorithm. + // If no bits and hasher are provided, a RSA key pair with 2048 bits and SHA256 hashing will be created. + RSA rsa = 1; + // Create a ECDSA key pair and specify the curve. + // If no curve is provided, a ECDSA key pair with P-256 curve will be created. + ECDSA ecdsa = 2; + // Create a ED25519 key pair. + ED25519 ed25519 = 3; + } + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"rsa\":{\"bits\":\"RSA_BITS_2048\",\"hasher\":\"RSA_HASHER_SHA256\"}}"; + }; +} + +message CreateWebKeyResponse { + // The unique identifier of the newly created key. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ActivateWebKeyRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message ActivateWebKeyResponse { + // The timestamp of the activation of the key. + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteWebKeyRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeleteWebKeyResponse { + // The timestamp of the deletion of the key. + // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. + // In case the deletion occurred in a previous request, the deletion date might be empty. + google.protobuf.Timestamp deletion_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListWebKeysRequest {} + +message ListWebKeysResponse { + repeated WebKey web_keys = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[{\"id\":\"69629012906488334\",\"creationDate\":\"2024-12-18T07:50:47.492Z\",\"changeDate\":\"2024-12-18T08:04:47.492Z\",\"state\":\"STATE_ACTIVE\",\"rsa\":{\"bits\":\"RSA_BITS_2048\",\"hasher\":\"RSA_HASHER_SHA256\"}},{\"id\":\"69629012909346200\",\"creationDate\":\"2025-01-18T12:05:47.492Z\",\"state\":\"STATE_INITIAL\",\"ecdsa\":{\"curve\":\"ECDSA_CURVE_P256\"}}]"; + } + ]; +} \ No newline at end of file