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/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/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 d7eabeb240..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) 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 b95cb6d74b..30e037c80f 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 @@ -1733,6 +1713,298 @@ InternalAuthZ: - "user.grant.read" - "user.membership.read" +SystemAuthZ: + RolePermissionMappings: + - Role: "SYSTEM_OWNER" + Permissions: + - "system.instance.read" + - "system.instance.write" + - "system.instance.delete" + - "system.domain.read" + - "system.domain.write" + - "system.domain.delete" + - "system.debug.read" + - "system.debug.write" + - "system.debug.delete" + - "system.feature.read" + - "system.feature.write" + - "system.feature.delete" + - "system.limits.write" + - "system.limits.delete" + - "system.quota.write" + - "system.quota.delete" + - "system.iam.member.read" + - Role: "SYSTEM_OWNER_VIEWER" + Permissions: + - "system.instance.read" + - "system.domain.read" + - "system.debug.read" + - "system.feature.read" + - "system.iam.member.read" + - Role: "IAM_OWNER" + Permissions: + - "iam.read" + - "iam.write" + - "iam.policy.read" + - "iam.policy.write" + - "iam.policy.delete" + - "iam.member.read" + - "iam.member.write" + - "iam.member.delete" + - "iam.idp.read" + - "iam.idp.write" + - "iam.idp.delete" + - "iam.action.read" + - "iam.action.write" + - "iam.action.delete" + - "iam.flow.read" + - "iam.flow.write" + - "iam.flow.delete" + - "iam.feature.read" + - "iam.feature.write" + - "iam.feature.delete" + - "iam.restrictions.read" + - "iam.restrictions.write" + - "iam.web_key.write" + - "iam.web_key.delete" + - "iam.web_key.read" + - "iam.debug.write" + - "iam.debug.read" + - "org.read" + - "org.global.read" + - "org.create" + - "org.write" + - "org.delete" + - "org.member.read" + - "org.member.write" + - "org.member.delete" + - "org.idp.read" + - "org.idp.write" + - "org.idp.delete" + - "org.action.read" + - "org.action.write" + - "org.action.delete" + - "org.flow.read" + - "org.flow.write" + - "org.flow.delete" + - "org.feature.read" + - "org.feature.write" + - "org.feature.delete" + - "user.read" + - "user.global.read" + - "user.write" + - "user.delete" + - "user.grant.read" + - "user.grant.write" + - "user.grant.delete" + - "user.membership.read" + - "user.credential.write" + - "user.passkey.write" + - "user.feature.read" + - "user.feature.write" + - "user.feature.delete" + - "policy.read" + - "policy.write" + - "policy.delete" + - "project.read" + - "project.create" + - "project.write" + - "project.delete" + - "project.member.read" + - "project.member.write" + - "project.member.delete" + - "project.role.read" + - "project.role.write" + - "project.role.delete" + - "project.app.read" + - "project.app.write" + - "project.app.delete" + - "project.grant.read" + - "project.grant.write" + - "project.grant.delete" + - "project.grant.member.read" + - "project.grant.member.write" + - "project.grant.member.delete" + - "events.read" + - "milestones.read" + - "session.read" + - "session.delete" + - "action.target.read" + - "action.target.write" + - "action.target.delete" + - "action.execution.read" + - "action.execution.write" + - "userschema.read" + - "userschema.write" + - "userschema.delete" + - "session.read" + - "session.delete" + - Role: "IAM_OWNER_VIEWER" + Permissions: + - "iam.read" + - "iam.policy.read" + - "iam.member.read" + - "iam.idp.read" + - "iam.action.read" + - "iam.flow.read" + - "iam.restrictions.read" + - "iam.feature.read" + - "iam.web_key.read" + - "iam.debug.read" + - "org.read" + - "org.member.read" + - "org.idp.read" + - "org.action.read" + - "org.flow.read" + - "org.feature.read" + - "user.read" + - "user.global.read" + - "user.grant.read" + - "user.membership.read" + - "user.feature.read" + - "policy.read" + - "project.read" + - "project.member.read" + - "project.role.read" + - "project.app.read" + - "project.grant.read" + - "project.grant.member.read" + - "events.read" + - "milestones.read" + - "action.target.read" + - "action.execution.read" + - "userschema.read" + - "session.read" + - Role: "IAM_ORG_MANAGER" + Permissions: + - "org.read" + - "org.global.read" + - "org.create" + - "org.write" + - "org.delete" + - "org.member.read" + - "org.member.write" + - "org.member.delete" + - "org.idp.read" + - "org.idp.write" + - "org.idp.delete" + - "org.action.read" + - "org.action.write" + - "org.action.delete" + - "org.flow.read" + - "org.flow.write" + - "org.flow.delete" + - "org.feature.read" + - "org.feature.write" + - "org.feature.delete" + - "user.read" + - "user.global.read" + - "user.write" + - "user.delete" + - "user.grant.read" + - "user.grant.write" + - "user.grant.delete" + - "user.membership.read" + - "user.credential.write" + - "user.passkey.write" + - "user.feature.read" + - "user.feature.write" + - "user.feature.delete" + - "policy.read" + - "policy.write" + - "policy.delete" + - "project.read" + - "project.create" + - "project.write" + - "project.delete" + - "project.member.read" + - "project.member.write" + - "project.member.delete" + - "project.role.read" + - "project.role.write" + - "project.role.delete" + - "project.app.read" + - "project.app.write" + - "project.app.delete" + - "project.grant.read" + - "project.grant.write" + - "project.grant.delete" + - "project.grant.member.read" + - "project.grant.member.write" + - "project.grant.member.delete" + - "session.delete" + - Role: "IAM_USER_MANAGER" + Permissions: + - "org.read" + - "org.global.read" + - "org.member.read" + - "org.member.delete" + - "user.read" + - "user.global.read" + - "user.write" + - "user.delete" + - "user.grant.read" + - "user.grant.write" + - "user.grant.delete" + - "user.membership.read" + - "user.passkey.write" + - "user.feature.read" + - "user.feature.write" + - "user.feature.delete" + - "project.read" + - "project.member.read" + - "project.role.read" + - "project.app.read" + - "project.grant.read" + - "project.grant.write" + - "project.grant.delete" + - "project.grant.member.read" + - "session.delete" + - Role: "IAM_ADMIN_IMPERSONATOR" + Permissions: + - "admin.impersonation" + - "impersonation" + - Role: "IAM_END_USER_IMPERSONATOR" + Permissions: + - "impersonation" + - Role: "IAM_LOGIN_CLIENT" + Permissions: + - "iam.read" + - "iam.policy.read" + - "iam.member.read" + - "iam.member.write" + - "iam.idp.read" + - "iam.feature.read" + - "iam.restrictions.read" + - "org.read" + - "org.member.read" + - "org.member.write" + - "org.idp.read" + - "org.feature.read" + - "user.read" + - "user.write" + - "user.grant.read" + - "user.grant.write" + - "user.membership.read" + - "user.credential.write" + - "user.passkey.write" + - "user.feature.read" + - "policy.read" + - "project.read" + - "project.member.read" + - "project.member.write" + - "project.role.read" + - "project.app.read" + - "project.member.read" + - "project.member.write" + - "project.grant.read" + - "project.grant.member.read" + - "project.grant.member.write" + - "session.read" + - "session.link" + - "session.delete" + - "userschema.read" + # If a new projection is introduced it will be prefilled during the setup process (if enabled) # This can prevent serving outdated data after a version upgrade, but might require a longer setup / upgrade process: # https://zitadel.com/docs/self-hosting/manage/updating_scaling diff --git a/cmd/initialise/config.go b/cmd/initialise/config.go index 3fe7173860..899018ddcb 100644 --- a/cmd/initialise/config.go +++ b/cmd/initialise/config.go @@ -19,7 +19,7 @@ func MustNewConfig(v *viper.Viper) *Config { config := new(Config) err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( - database.DecodeHook, + database.DecodeHook(false), mapstructure.TextUnmarshallerHookFunc(), )), ) diff --git a/cmd/initialise/init.go b/cmd/initialise/init.go index 02fd481eab..cc505325a9 100644 --- a/cmd/initialise/init.go +++ b/cmd/initialise/init.go @@ -12,20 +12,17 @@ import ( ) var ( - //go:embed sql/cockroach/* - //go:embed sql/postgres/* + //go:embed sql/*.sql stmts embed.FS createUserStmt string grantStmt string - settingsStmt string databaseStmt string createEventstoreStmt string createProjectionsStmt string createSystemStmt string createEncryptionKeysStmt string createEventsStmt string - createSystemSequenceStmt string createUniqueConstraints string roleAlreadyExistsCode = "42710" @@ -39,7 +36,7 @@ func New() *cobra.Command { Long: `Sets up the minimum requirements to start ZITADEL. Prerequisites: -- database (PostgreSql or cockroachdb) +- PostgreSql database The user provided by flags needs privileges to - create the database if it does not exist @@ -53,7 +50,7 @@ The user provided by flags needs privileges to }, } - cmd.AddCommand(newZitadel(), newDatabase(), newUser(), newGrant(), newSettings()) + cmd.AddCommand(newZitadel(), newDatabase(), newUser(), newGrant()) return cmd } @@ -62,7 +59,6 @@ func InitAll(ctx context.Context, config *Config) { VerifyUser(config.Database.Username(), config.Database.Password()), VerifyDatabase(config.Database.DatabaseName()), VerifyGrant(config.Database.DatabaseName(), config.Database.Username()), - VerifySettings(config.Database.DatabaseName(), config.Database.Username()), ) logging.OnError(err).Fatal("unable to initialize the database") @@ -73,7 +69,7 @@ func InitAll(ctx context.Context, config *Config) { func initialise(ctx context.Context, config database.Config, steps ...func(context.Context, *database.DB) error) error { logging.Info("initialization started") - err := ReadStmts(config.Type()) + err := ReadStmts() if err != nil { return err } @@ -97,58 +93,48 @@ func Init(ctx context.Context, db *database.DB, steps ...func(context.Context, * return nil } -func ReadStmts(typ string) (err error) { - createUserStmt, err = readStmt(typ, "01_user") +func ReadStmts() (err error) { + createUserStmt, err = readStmt("01_user") if err != nil { return err } - databaseStmt, err = readStmt(typ, "02_database") + databaseStmt, err = readStmt("02_database") if err != nil { return err } - grantStmt, err = readStmt(typ, "03_grant_user") + grantStmt, err = readStmt("03_grant_user") if err != nil { return err } - createEventstoreStmt, err = readStmt(typ, "04_eventstore") + createEventstoreStmt, err = readStmt("04_eventstore") if err != nil { return err } - createProjectionsStmt, err = readStmt(typ, "05_projections") + createProjectionsStmt, err = readStmt("05_projections") if err != nil { return err } - createSystemStmt, err = readStmt(typ, "06_system") + createSystemStmt, err = readStmt("06_system") if err != nil { return err } - createEncryptionKeysStmt, err = readStmt(typ, "07_encryption_keys_table") + createEncryptionKeysStmt, err = readStmt("07_encryption_keys_table") if err != nil { return err } - createEventsStmt, err = readStmt(typ, "08_events_table") + createEventsStmt, err = readStmt("08_events_table") if err != nil { return err } - createSystemSequenceStmt, err = readStmt(typ, "09_system_sequence") - if err != nil { - return err - } - - createUniqueConstraints, err = readStmt(typ, "10_unique_constraints_table") - if err != nil { - return err - } - - settingsStmt, err = readStmt(typ, "11_settings") + createUniqueConstraints, err = readStmt("10_unique_constraints_table") if err != nil { return err } @@ -156,7 +142,7 @@ func ReadStmts(typ string) (err error) { return nil } -func readStmt(typ, step string) (string, error) { - stmt, err := stmts.ReadFile("sql/" + typ + "/" + step + ".sql") +func readStmt(step string) (string, error) { + stmt, err := stmts.ReadFile("sql/" + step + ".sql") return string(stmt), err } diff --git a/cmd/initialise/sql/cockroach/01_user.sql b/cmd/initialise/sql/01_user.sql similarity index 56% rename from cmd/initialise/sql/cockroach/01_user.sql rename to cmd/initialise/sql/01_user.sql index 4e621216ce..7be2d4ae4d 100644 --- a/cmd/initialise/sql/cockroach/01_user.sql +++ b/cmd/initialise/sql/01_user.sql @@ -1,2 +1,2 @@ -- replace %[1]s with the name of the user -CREATE USER IF NOT EXISTS "%[1]s" \ No newline at end of file +CREATE USER "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/02_database.sql b/cmd/initialise/sql/02_database.sql similarity index 54% rename from cmd/initialise/sql/cockroach/02_database.sql rename to cmd/initialise/sql/02_database.sql index 6103b95b31..172913661b 100644 --- a/cmd/initialise/sql/cockroach/02_database.sql +++ b/cmd/initialise/sql/02_database.sql @@ -1,2 +1,2 @@ -- replace %[1]s with the name of the database -CREATE DATABASE IF NOT EXISTS "%[1]s"; +CREATE DATABASE "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/03_grant_user.sql b/cmd/initialise/sql/03_grant_user.sql similarity index 100% rename from cmd/initialise/sql/postgres/03_grant_user.sql rename to cmd/initialise/sql/03_grant_user.sql diff --git a/cmd/initialise/sql/cockroach/04_eventstore.sql b/cmd/initialise/sql/04_eventstore.sql similarity index 100% rename from cmd/initialise/sql/cockroach/04_eventstore.sql rename to cmd/initialise/sql/04_eventstore.sql diff --git a/cmd/initialise/sql/cockroach/05_projections.sql b/cmd/initialise/sql/05_projections.sql similarity index 100% rename from cmd/initialise/sql/cockroach/05_projections.sql rename to cmd/initialise/sql/05_projections.sql diff --git a/cmd/initialise/sql/cockroach/06_system.sql b/cmd/initialise/sql/06_system.sql similarity index 100% rename from cmd/initialise/sql/cockroach/06_system.sql rename to cmd/initialise/sql/06_system.sql diff --git a/cmd/initialise/sql/cockroach/07_encryption_keys_table.sql b/cmd/initialise/sql/07_encryption_keys_table.sql similarity index 100% rename from cmd/initialise/sql/cockroach/07_encryption_keys_table.sql rename to cmd/initialise/sql/07_encryption_keys_table.sql diff --git a/cmd/initialise/sql/postgres/08_events_table.sql b/cmd/initialise/sql/08_events_table.sql similarity index 100% rename from cmd/initialise/sql/postgres/08_events_table.sql rename to cmd/initialise/sql/08_events_table.sql diff --git a/cmd/initialise/sql/postgres/10_unique_constraints_table.sql b/cmd/initialise/sql/10_unique_constraints_table.sql similarity index 100% rename from cmd/initialise/sql/postgres/10_unique_constraints_table.sql rename to cmd/initialise/sql/10_unique_constraints_table.sql diff --git a/cmd/initialise/sql/README.md b/cmd/initialise/sql/README.md index b477c0fb73..b7a18f0f98 100644 --- a/cmd/initialise/sql/README.md +++ b/cmd/initialise/sql/README.md @@ -11,6 +11,5 @@ The sql-files in this folder initialize the ZITADEL database and user. These obj - 05_projections.sql: creates the schema needed to read the data - 06_system.sql: creates the schema needed for ZITADEL itself - 07_encryption_keys_table.sql: creates the table for encryption keys (for event data) -- files 08_enable_hash_sharded_indexes.sql and 09_events_table.sql must run in the same session - - 08_enable_hash_sharded_indexes.sql enables the [hash sharded index](https://www.cockroachlabs.com/docs/stable/hash-sharded-indexes.html) feature for this session - - 09_events_table.sql creates the table for eventsourcing +- 08_events_table.sql creates the table for eventsourcing +- 10_unique_constraints_table.sql creates the table to check unique constraints for events diff --git a/cmd/initialise/sql/cockroach/03_grant_user.sql b/cmd/initialise/sql/cockroach/03_grant_user.sql deleted file mode 100644 index de0d2743eb..0000000000 --- a/cmd/initialise/sql/cockroach/03_grant_user.sql +++ /dev/null @@ -1,4 +0,0 @@ --- replace the first %[1]s with the database --- replace the second \%[2]s with the user -GRANT ALL ON DATABASE "%[1]s" TO "%[2]s"; -GRANT SYSTEM VIEWACTIVITY TO "%[2]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/08_events_table.sql b/cmd/initialise/sql/cockroach/08_events_table.sql deleted file mode 100644 index ebaf18ce2a..0000000000 --- a/cmd/initialise/sql/cockroach/08_events_table.sql +++ /dev/null @@ -1,116 +0,0 @@ -CREATE TABLE IF NOT EXISTS eventstore.events2 ( - instance_id TEXT NOT NULL - , aggregate_type TEXT NOT NULL - , aggregate_id TEXT NOT NULL - - , event_type TEXT NOT NULL - , "sequence" BIGINT NOT NULL - , revision SMALLINT NOT NULL - , created_at TIMESTAMPTZ NOT NULL - , payload JSONB - , creator TEXT NOT NULL - , "owner" TEXT NOT NULL - - , "position" DECIMAL NOT NULL - , in_tx_order INTEGER NOT NULL - - , PRIMARY KEY (instance_id, aggregate_type, aggregate_id, "sequence") - , INDEX es_active_instances (created_at DESC) STORING ("position") - , INDEX es_wm (aggregate_id, instance_id, aggregate_type, event_type) - , INDEX es_projection (instance_id, aggregate_type, event_type, "position" DESC) -); - --- represents an event to be created. -CREATE TYPE IF NOT EXISTS eventstore.command AS ( - instance_id TEXT - , aggregate_type TEXT - , aggregate_id TEXT - , command_type TEXT - , revision INT2 - , payload JSONB - , creator TEXT - , owner TEXT -); - -CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$ -SELECT - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - , ("c").command_type AS event_type - , cs.sequence + ROW_NUMBER() OVER (PARTITION BY ("c").instance_id, ("c").aggregate_type, ("c").aggregate_id ORDER BY ("c").in_tx_order) AS sequence - , ("c").revision - , hlc_to_timestamp(cluster_logical_timestamp()) AS created_at - , ("c").payload - , ("c").creator - , cs.owner - , cluster_logical_timestamp() AS position - , ("c").in_tx_order -FROM ( - SELECT - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - , ("c").command_type - , ("c").revision - , ("c").payload - , ("c").creator - , ("c").owner - , ROW_NUMBER() OVER () AS in_tx_order - FROM - UNNEST(commands) AS "c" -) AS "c" -JOIN ( - SELECT - cmds.instance_id - , cmds.aggregate_type - , cmds.aggregate_id - , CASE WHEN (e.owner IS NOT NULL OR e.owner <> '') THEN e.owner ELSE command_owners.owner END AS owner - , COALESCE(MAX(e.sequence), 0) AS sequence - FROM ( - SELECT DISTINCT - ("cmds").instance_id - , ("cmds").aggregate_type - , ("cmds").aggregate_id - , ("cmds").owner - FROM UNNEST(commands) AS "cmds" - ) AS cmds - LEFT JOIN eventstore.events2 AS e - ON cmds.instance_id = e.instance_id - AND cmds.aggregate_type = e.aggregate_type - AND cmds.aggregate_id = e.aggregate_id - JOIN ( - SELECT - DISTINCT ON ( - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - ) - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - , ("c").owner - FROM - UNNEST(commands) AS "c" - ) AS command_owners ON - cmds.instance_id = command_owners.instance_id - AND cmds.aggregate_type = command_owners.aggregate_type - AND cmds.aggregate_id = command_owners.aggregate_id - GROUP BY - cmds.instance_id - , cmds.aggregate_type - , cmds.aggregate_id - , 4 -- owner -) AS cs - ON ("c").instance_id = cs.instance_id - AND ("c").aggregate_type = cs.aggregate_type - AND ("c").aggregate_id = cs.aggregate_id -ORDER BY - in_tx_order -$$ LANGUAGE SQL; - -CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command[]) RETURNS SETOF eventstore.events2 AS $$ - INSERT INTO eventstore.events2 - SELECT * FROM eventstore.commands_to_events(commands) - RETURNING * -$$ LANGUAGE SQL; \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/09_system_sequence.sql b/cmd/initialise/sql/cockroach/09_system_sequence.sql deleted file mode 100644 index 596e887664..0000000000 --- a/cmd/initialise/sql/cockroach/09_system_sequence.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE SEQUENCE IF NOT EXISTS eventstore.system_seq diff --git a/cmd/initialise/sql/cockroach/10_unique_constraints_table.sql b/cmd/initialise/sql/cockroach/10_unique_constraints_table.sql deleted file mode 100644 index 2594a248b7..0000000000 --- a/cmd/initialise/sql/cockroach/10_unique_constraints_table.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS eventstore.unique_constraints ( - instance_id TEXT, - unique_type TEXT, - unique_field TEXT, - PRIMARY KEY (instance_id, unique_type, unique_field) -) diff --git a/cmd/initialise/sql/cockroach/11_settings.sql b/cmd/initialise/sql/cockroach/11_settings.sql deleted file mode 100644 index 5fa9dd72f6..0000000000 --- a/cmd/initialise/sql/cockroach/11_settings.sql +++ /dev/null @@ -1,4 +0,0 @@ --- replace the first %[1]q with the database in double quotes --- replace the second \%[2]q with the user in double quotes$ --- For more information see technical advisory 10009 (https://zitadel.com/docs/support/advisory/a10009) -ALTER ROLE %[2]q IN DATABASE %[1]q SET enable_durable_locking_for_serializable = on; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/01_user.sql b/cmd/initialise/sql/postgres/01_user.sql deleted file mode 100644 index cd60b9a2cf..0000000000 --- a/cmd/initialise/sql/postgres/01_user.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE USER "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/02_database.sql b/cmd/initialise/sql/postgres/02_database.sql deleted file mode 100644 index 895a1f29d5..0000000000 --- a/cmd/initialise/sql/postgres/02_database.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE DATABASE "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/04_eventstore.sql b/cmd/initialise/sql/postgres/04_eventstore.sql deleted file mode 100644 index 3cb4fc0d3e..0000000000 --- a/cmd/initialise/sql/postgres/04_eventstore.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS eventstore; - -GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/05_projections.sql b/cmd/initialise/sql/postgres/05_projections.sql deleted file mode 100644 index 91ca6662ee..0000000000 --- a/cmd/initialise/sql/postgres/05_projections.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS projections; - -GRANT ALL ON ALL TABLES IN SCHEMA projections TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/06_system.sql b/cmd/initialise/sql/postgres/06_system.sql deleted file mode 100644 index 6c9138918b..0000000000 --- a/cmd/initialise/sql/postgres/06_system.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS system; - -GRANT ALL ON ALL TABLES IN SCHEMA system TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/07_encryption_keys_table.sql b/cmd/initialise/sql/postgres/07_encryption_keys_table.sql deleted file mode 100644 index 61cb617fdf..0000000000 --- a/cmd/initialise/sql/postgres/07_encryption_keys_table.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS system.encryption_keys ( - id TEXT NOT NULL - , key TEXT NOT NULL - - , PRIMARY KEY (id) -); diff --git a/cmd/initialise/sql/postgres/09_system_sequence.sql b/cmd/initialise/sql/postgres/09_system_sequence.sql deleted file mode 100644 index 15383b3878..0000000000 --- a/cmd/initialise/sql/postgres/09_system_sequence.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE SEQUENCE IF NOT EXISTS eventstore.system_seq; diff --git a/cmd/initialise/verify_database.go b/cmd/initialise/verify_database.go index 6e04e489f5..3e3bea9efa 100644 --- a/cmd/initialise/verify_database.go +++ b/cmd/initialise/verify_database.go @@ -19,7 +19,7 @@ func newDatabase() *cobra.Command { Long: `Sets up the ZITADEL database. Prerequisites: -- cockroachDB or postgreSQL +- postgreSQL The user provided by flags needs privileges to - create the database if it does not exist diff --git a/cmd/initialise/verify_database_test.go b/cmd/initialise/verify_database_test.go index d7da97847f..1899605e4f 100644 --- a/cmd/initialise/verify_database_test.go +++ b/cmd/initialise/verify_database_test.go @@ -8,7 +8,7 @@ import ( ) func Test_verifyDB(t *testing.T) { - err := ReadStmts("cockroach") //TODO: check all dialects + err := ReadStmts() if err != nil { t.Errorf("unable to read stmts: %v", err) t.FailNow() @@ -27,7 +27,7 @@ func Test_verifyDB(t *testing.T) { name: "doesn't exists, create fails", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", sql.ErrTxDone), + expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE \"zitadel\"", sql.ErrTxDone), ), database: "zitadel", }, @@ -37,7 +37,7 @@ func Test_verifyDB(t *testing.T) { name: "doesn't exists, create successful", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", nil), + expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE \"zitadel\"", nil), ), database: "zitadel", }, @@ -47,7 +47,7 @@ func Test_verifyDB(t *testing.T) { name: "already exists", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", nil), + expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE \"zitadel\"", nil), ), database: "zitadel", }, diff --git a/cmd/initialise/verify_grant.go b/cmd/initialise/verify_grant.go index a14a495bff..27f0bd4d08 100644 --- a/cmd/initialise/verify_grant.go +++ b/cmd/initialise/verify_grant.go @@ -19,7 +19,7 @@ func newGrant() *cobra.Command { Long: `Sets ALL grant to the database user. Prerequisites: -- cockroachDB or postgreSQL +- postgreSQL `, Run: func(cmd *cobra.Command, args []string) { config := MustNewConfig(viper.GetViper()) diff --git a/cmd/initialise/verify_settings.go b/cmd/initialise/verify_settings.go deleted file mode 100644 index 6f4ba7c074..0000000000 --- a/cmd/initialise/verify_settings.go +++ /dev/null @@ -1,45 +0,0 @@ -package initialise - -import ( - "context" - _ "embed" - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/internal/database" -) - -func newSettings() *cobra.Command { - return &cobra.Command{ - Use: "settings", - Short: "Ensures proper settings on the database", - Long: `Ensures proper settings on the database. - -Prerequisites: -- cockroachDB or postgreSQL - -Cockroach -- Sets enable_durable_locking_for_serializable to on for the zitadel user and database -`, - Run: func(cmd *cobra.Command, args []string) { - config := MustNewConfig(viper.GetViper()) - - err := initialise(cmd.Context(), config.Database, VerifySettings(config.Database.DatabaseName(), config.Database.Username())) - logging.OnError(err).Fatal("unable to set settings") - }, - } -} - -func VerifySettings(databaseName, username string) func(context.Context, *database.DB) error { - return func(ctx context.Context, db *database.DB) error { - if db.Type() == "postgres" { - return nil - } - logging.WithFields("user", username, "database", databaseName).Info("verify settings") - - return exec(ctx, db, fmt.Sprintf(settingsStmt, databaseName, username), nil) - } -} diff --git a/cmd/initialise/verify_user.go b/cmd/initialise/verify_user.go index 43bdb91420..3adca93e53 100644 --- a/cmd/initialise/verify_user.go +++ b/cmd/initialise/verify_user.go @@ -19,7 +19,7 @@ func newUser() *cobra.Command { Long: `Sets up the ZITADEL database user. Prerequisites: -- cockroachDB or postgreSQL +- postgreSQL The user provided by flags needs privileges to - create the database if it does not exist diff --git a/cmd/initialise/verify_user_test.go b/cmd/initialise/verify_user_test.go index 53b35e67db..40cde5baa2 100644 --- a/cmd/initialise/verify_user_test.go +++ b/cmd/initialise/verify_user_test.go @@ -8,7 +8,7 @@ import ( ) func Test_verifyUser(t *testing.T) { - err := ReadStmts("cockroach") //TODO: check all dialects + err := ReadStmts() if err != nil { t.Errorf("unable to read stmts: %v", err) t.FailNow() @@ -28,7 +28,7 @@ func Test_verifyUser(t *testing.T) { name: "doesn't exists, create fails", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\"", sql.ErrTxDone), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\"", sql.ErrTxDone), ), username: "zitadel-user", password: "", @@ -39,7 +39,7 @@ func Test_verifyUser(t *testing.T) { name: "correct without password", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\"", nil), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\"", nil), ), username: "zitadel-user", password: "", @@ -50,7 +50,7 @@ func Test_verifyUser(t *testing.T) { name: "correct with password", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\" WITH PASSWORD 'password'", nil), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\" WITH PASSWORD 'password'", nil), ), username: "zitadel-user", password: "password", @@ -61,7 +61,7 @@ func Test_verifyUser(t *testing.T) { name: "already exists", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\" WITH PASSWORD 'password'", nil), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\" WITH PASSWORD 'password'", nil), ), username: "zitadel-user", password: "", diff --git a/cmd/initialise/verify_zitadel.go b/cmd/initialise/verify_zitadel.go index 1ae85a21fa..78f28809c2 100644 --- a/cmd/initialise/verify_zitadel.go +++ b/cmd/initialise/verify_zitadel.go @@ -21,7 +21,7 @@ func newZitadel() *cobra.Command { Long: `initialize ZITADEL internals. Prerequisites: -- cockroachDB or postgreSQL with user and database +- postgreSQL with user and database `, Run: func(cmd *cobra.Command, args []string) { config := MustNewConfig(viper.GetViper()) @@ -32,7 +32,7 @@ Prerequisites: } func VerifyZitadel(ctx context.Context, db *database.DB, config database.Config) error { - err := ReadStmts(config.Type()) + err := ReadStmts() if err != nil { return err } @@ -68,11 +68,6 @@ func VerifyZitadel(ctx context.Context, db *database.DB, config database.Config) return err } - logging.WithFields().Info("verify system sequence") - if err := exec(ctx, conn, createSystemSequenceStmt, nil); err != nil { - return err - } - logging.WithFields().Info("verify unique constraints") if err := exec(ctx, conn, createUniqueConstraints, nil); err != nil { return err diff --git a/cmd/initialise/verify_zitadel_test.go b/cmd/initialise/verify_zitadel_test.go index 194911a179..7fccd4c0a2 100644 --- a/cmd/initialise/verify_zitadel_test.go +++ b/cmd/initialise/verify_zitadel_test.go @@ -9,7 +9,7 @@ import ( ) func Test_verifyEvents(t *testing.T) { - err := ReadStmts("cockroach") //TODO: check all dialects + err := ReadStmts() if err != nil { t.Errorf("unable to read stmts: %v", err) t.FailNow() diff --git a/cmd/key/key.go b/cmd/key/key.go index 1dba8fd969..a1cf15b34e 100644 --- a/cmd/key/key.go +++ b/cmd/key/key.go @@ -40,7 +40,7 @@ func newKey() *cobra.Command { Long: `create new encryption key(s) (encrypted by the provided master key) provide key(s) by YAML file and/or by argument Requirements: -- cockroachdb`, +- postgreSQL`, Example: `new -f keys.yaml new key1=somekey key2=anotherkey new -f keys.yaml key2=anotherkey`, diff --git a/cmd/mirror/config.go b/cmd/mirror/config.go index cc98000869..89b0876e5f 100644 --- a/cmd/mirror/config.go +++ b/cmd/mirror/config.go @@ -71,7 +71,7 @@ func mustNewConfig(v *viper.Viper, config any) { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), - database.DecodeHook, + database.DecodeHook(true), actions.HTTPConfigDecodeHook, hook.EnumHookFunc(internal_authz.MemberTypeString), mapstructure.TextUnmarshallerHookFunc(), diff --git a/cmd/mirror/defaults.yaml b/cmd/mirror/defaults.yaml index 7db91ecc0b..4b42c06534 100644 --- a/cmd/mirror/defaults.yaml +++ b/cmd/mirror/defaults.yaml @@ -5,8 +5,6 @@ Source: Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE MaxOpenConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS MaxIdleConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS - EventPushConnRatio: 0.33 # ZITADEL_DATABASE_COCKROACH_EVENTPUSHCONNRATIO - ProjectionSpoolerConnRatio: 0.33 # ZITADEL_DATABASE_COCKROACH_PROJECTIONSPOOLERCONNRATIO MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS @@ -39,44 +37,23 @@ Source: Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY Destination: - cockroach: - Host: localhost # ZITADEL_DATABASE_COCKROACH_HOST - Port: 26257 # ZITADEL_DATABASE_COCKROACH_PORT - Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE - MaxOpenConns: 0 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS - MaxIdleConns: 0 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS - MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME - MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME - EventPushConnRatio: 0.01 # ZITADEL_DATABASE_COCKROACH_EVENTPUSHCONNRATIO - ProjectionSpoolerConnRatio: 0.5 # ZITADEL_DATABASE_COCKROACH_PROJECTIONSPOOLERCONNRATIO - Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS - User: - Username: zitadel # ZITADEL_DATABASE_COCKROACH_USER_USERNAME - Password: "" # ZITADEL_DATABASE_COCKROACH_USER_PASSWORD - SSL: - Mode: disable # ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE - RootCert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT - Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT - Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY - # Postgres is used as soon as a value is set - # The values describe the possible fields to set values postgres: - Host: # ZITADEL_DATABASE_POSTGRES_HOST - Port: # ZITADEL_DATABASE_POSTGRES_PORT - Database: # ZITADEL_DATABASE_POSTGRES_DATABASE - MaxOpenConns: # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS - MaxIdleConns: # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS - MaxConnLifetime: # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME - MaxConnIdleTime: # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME - Options: # ZITADEL_DATABASE_POSTGRES_OPTIONS + Host: localhost # ZITADEL_DATABASE_POSTGRES_HOST + Port: 5432 # ZITADEL_DATABASE_POSTGRES_PORT + Database: zitadel # ZITADEL_DATABASE_POSTGRES_DATABASE + MaxOpenConns: 5 # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS + MaxIdleConns: 2 # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS + MaxConnLifetime: 30m # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME + MaxConnIdleTime: 5m # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME + Options: "" # ZITADEL_DATABASE_POSTGRES_OPTIONS User: - Username: # ZITADEL_DATABASE_POSTGRES_USER_USERNAME - Password: # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD + Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME + Password: "" # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD SSL: - Mode: # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE - RootCert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT - Cert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT - Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY + Mode: disable # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE + RootCert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT + Cert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT + Key: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY EventBulkSize: 10000 diff --git a/cmd/mirror/event.go b/cmd/mirror/event.go index 2bb0d52f45..d513990e10 100644 --- a/cmd/mirror/event.go +++ b/cmd/mirror/event.go @@ -4,7 +4,6 @@ import ( "context" "github.com/zitadel/zitadel/internal/v2/eventstore" - "github.com/zitadel/zitadel/internal/v2/projection" "github.com/zitadel/zitadel/internal/v2/readmodel" "github.com/zitadel/zitadel/internal/v2/system" mirror_event "github.com/zitadel/zitadel/internal/v2/system/mirror" @@ -30,39 +29,6 @@ func queryLastSuccessfulMigration(ctx context.Context, destinationES *eventstore return lastSuccess, nil } -func writeMigrationStart(ctx context.Context, sourceES *eventstore.EventStore, id string, destination string) (_ float64, err error) { - var cmd *eventstore.Command - if len(instanceIDs) > 0 { - cmd, err = mirror_event.NewStartedInstancesCommand(destination, instanceIDs) - if err != nil { - return 0, err - } - } else { - cmd = mirror_event.NewStartedSystemCommand(destination) - } - - var position projection.HighestPosition - - err = sourceES.Push( - ctx, - eventstore.NewPushIntent( - system.AggregateInstance, - eventstore.AppendAggregate( - system.AggregateOwner, - system.AggregateType, - id, - eventstore.CurrentSequenceMatches(0), - eventstore.AppendCommands(cmd), - ), - eventstore.PushReducer(&position), - ), - ) - if err != nil { - return 0, err - } - return position.Position, nil -} - func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position float64) error { return destinationES.Push( ctx, diff --git a/cmd/mirror/event_store.go b/cmd/mirror/event_store.go index 3825462126..8ce53b150a 100644 --- a/cmd/mirror/event_store.go +++ b/cmd/mirror/event_store.go @@ -14,6 +14,7 @@ import ( "github.com/zitadel/logging" db "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/database/dialect" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/v2/database" "github.com/zitadel/zitadel/internal/v2/eventstore" @@ -57,9 +58,9 @@ func copyEventstore(ctx context.Context, config *Migration) { func positionQuery(db *db.DB) string { switch db.Type() { - case "postgres": + case dialect.DatabaseTypePostgres: return "SELECT EXTRACT(EPOCH FROM clock_timestamp())" - case "cockroach": + case dialect.DatabaseTypeCockroach: return "SELECT cluster_logical_timestamp()" default: logging.WithFields("db_type", db.Type()).Fatal("database type not recognized") @@ -80,9 +81,6 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { destConn, err := dest.Conn(ctx) logging.OnError(err).Fatal("unable to acquire dest connection") - sourceES := eventstore.NewEventstoreFromOne(postgres.New(source, &postgres.Config{ - MaxRetries: 3, - })) destinationES := eventstore.NewEventstoreFromOne(postgres.New(dest, &postgres.Config{ MaxRetries: 3, })) @@ -90,8 +88,14 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { previousMigration, err := queryLastSuccessfulMigration(ctx, destinationES, source.DatabaseName()) logging.OnError(err).Fatal("unable to query latest successful migration") - maxPosition, err := writeMigrationStart(ctx, sourceES, migrationID, dest.DatabaseName()) - logging.OnError(err).Fatal("unable to write migration started event") + var maxPosition float64 + err = source.QueryRowContext(ctx, + func(row *sql.Row) error { + return row.Scan(&maxPosition) + }, + "SELECT MAX(position) FROM eventstore.events2 "+instanceClause(), + ) + logging.OnError(err).Fatal("unable to query max position from source") logging.WithFields("from", previousMigration.Position, "to", maxPosition).Info("start event migration") diff --git a/cmd/mirror/mirror.go b/cmd/mirror/mirror.go index 3fbfe1ae94..866238e56e 100644 --- a/cmd/mirror/mirror.go +++ b/cmd/mirror/mirror.go @@ -56,7 +56,6 @@ Order of execution: copyEventstore(cmd.Context(), config) projections(cmd.Context(), projectionConfig, masterKey) - verifyMigration(cmd.Context(), config) }, } diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index c15747e74a..66b3fb1a26 100644 --- a/cmd/mirror/projections.go +++ b/cmd/mirror/projections.go @@ -84,6 +84,7 @@ type ProjectionsConfig struct { ExternalDomain string ExternalSecure bool InternalAuthZ internal_authz.Config + SystemAuthZ internal_authz.Config SystemDefaults systemdefaults.SystemDefaults Telemetry *handlers.TelemetryPusherConfig Login login.Config @@ -117,8 +118,11 @@ func projections( staticStorage, err := config.AssetStorage.NewStorage(client.DB) logging.OnError(err).Fatal("unable create static storage") - config.Eventstore.Querier = old_es.NewCRDB(client) - config.Eventstore.Pusher = new_es.NewEventstore(client) + newEventstore := new_es.NewEventstore(client) + config.Eventstore.Querier = old_es.NewPostgres(client) + config.Eventstore.Pusher = newEventstore + config.Eventstore.Searcher = newEventstore + es := eventstore.NewEventstore(config.Eventstore) esV4 := es_v4.NewEventstoreFromOne(es_v4_pg.New(client, &es_v4_pg.Config{ MaxRetries: config.Eventstore.MaxRetries, @@ -147,7 +151,7 @@ func projections( sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, 0, @@ -184,7 +188,7 @@ func projections( keys.Target, &http.Client{}, func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) }, sessionTokenVerifier, config.OIDC.DefaultAccessTokenLifetime, @@ -220,7 +224,6 @@ func projections( keys.SMS, keys.OIDC, config.OIDC.DefaultBackChannelLogoutLifetime, - client, nil, ) @@ -248,7 +251,7 @@ func projections( } }() - for i := 0; i < int(config.Projections.ConcurrentInstances); i++ { + for range int(config.Projections.ConcurrentInstances) { go execProjections(ctx, instances, failedInstances, &wg) } @@ -270,31 +273,39 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc err := projection.ProjectInstance(ctx) if err != nil { - logging.WithFields("instance", instance).OnError(err).Info("trigger failed") + logging.WithFields("instance", instance).WithError(err).Info("trigger failed") + failedInstances <- instance + continue + } + + err = projection.ProjectInstanceFields(ctx) + if err != nil { + logging.WithFields("instance", instance).WithError(err).Info("trigger fields failed") failedInstances <- instance continue } err = admin_handler.ProjectInstance(ctx) if err != nil { - logging.WithFields("instance", instance).OnError(err).Info("trigger admin handler failed") + logging.WithFields("instance", instance).WithError(err).Info("trigger admin handler failed") failedInstances <- instance continue } err = auth_handler.ProjectInstance(ctx) if err != nil { - logging.WithFields("instance", instance).OnError(err).Info("trigger auth handler failed") + logging.WithFields("instance", instance).WithError(err).Info("trigger auth handler failed") failedInstances <- instance continue } err = notification.ProjectInstance(ctx) if err != nil { - logging.WithFields("instance", instance).OnError(err).Info("trigger notification failed") + logging.WithFields("instance", instance).WithError(err).Info("trigger notification failed") failedInstances <- instance continue } + logging.WithFields("instance", instance).Info("projections done") } wg.Done() diff --git a/cmd/setup/07.go b/cmd/setup/07.go index 73b9d3480b..590b220eb3 100644 --- a/cmd/setup/07.go +++ b/cmd/setup/07.go @@ -3,7 +3,7 @@ package setup import ( "context" "database/sql" - "embed" + _ "embed" "strings" "github.com/zitadel/zitadel/internal/eventstore" @@ -12,31 +12,20 @@ import ( var ( //go:embed 07/logstore.sql createLogstoreSchema07 string - //go:embed 07/cockroach/access.sql - //go:embed 07/postgres/access.sql - createAccessLogsTable07 embed.FS - //go:embed 07/cockroach/execution.sql - //go:embed 07/postgres/execution.sql - createExecutionLogsTable07 embed.FS + //go:embed 07/access.sql + createAccessLogsTable07 string + //go:embed 07/execution.sql + createExecutionLogsTable07 string ) type LogstoreTables struct { dbClient *sql.DB username string - dbType string } func (mig *LogstoreTables) Execute(ctx context.Context, _ eventstore.Event) error { - accessStmt, err := readStmt(createAccessLogsTable07, "07", mig.dbType, "access.sql") - if err != nil { - return err - } - executionStmt, err := readStmt(createExecutionLogsTable07, "07", mig.dbType, "execution.sql") - if err != nil { - return err - } - stmt := strings.ReplaceAll(createLogstoreSchema07, "%[1]s", mig.username) + accessStmt + executionStmt - _, err = mig.dbClient.ExecContext(ctx, stmt) + stmt := strings.ReplaceAll(createLogstoreSchema07, "%[1]s", mig.username) + createAccessLogsTable07 + createExecutionLogsTable07 + _, err := mig.dbClient.ExecContext(ctx, stmt) return err } diff --git a/cmd/setup/07/postgres/access.sql b/cmd/setup/07/access.sql similarity index 100% rename from cmd/setup/07/postgres/access.sql rename to cmd/setup/07/access.sql diff --git a/cmd/setup/07/cockroach/access.sql b/cmd/setup/07/cockroach/access.sql deleted file mode 100644 index fc5354cf32..0000000000 --- a/cmd/setup/07/cockroach/access.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE IF NOT EXISTS logstore.access ( - log_date TIMESTAMPTZ NOT NULL - , protocol INT NOT NULL - , request_url TEXT NOT NULL - , response_status INT NOT NULL - , request_headers JSONB - , response_headers JSONB - , instance_id TEXT NOT NULL - , project_id TEXT NOT NULL - , requested_domain TEXT - , requested_host TEXT - - , INDEX protocol_date_desc (instance_id, protocol, log_date DESC) STORING (request_url, response_status, request_headers) -); diff --git a/cmd/setup/07/cockroach/execution.sql b/cmd/setup/07/cockroach/execution.sql deleted file mode 100644 index b8e18b525a..0000000000 --- a/cmd/setup/07/cockroach/execution.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE IF NOT EXISTS logstore.execution ( - log_date TIMESTAMPTZ NOT NULL - , took INTERVAL - , message TEXT NOT NULL - , loglevel INT NOT NULL - , instance_id TEXT NOT NULL - , action_id TEXT NOT NULL - , metadata JSONB - - , INDEX log_date_desc (instance_id, log_date DESC) STORING (took) -); diff --git a/cmd/setup/07/postgres/execution.sql b/cmd/setup/07/execution.sql similarity index 100% rename from cmd/setup/07/postgres/execution.sql rename to cmd/setup/07/execution.sql diff --git a/cmd/setup/08.go b/cmd/setup/08.go index bec6a65ebb..fa006bd3cf 100644 --- a/cmd/setup/08.go +++ b/cmd/setup/08.go @@ -2,16 +2,15 @@ package setup import ( "context" - "embed" + _ "embed" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" ) var ( - //go:embed 08/cockroach/08.sql - //go:embed 08/postgres/08.sql - tokenIndexes08 embed.FS + //go:embed 08/08.sql + tokenIndexes08 string ) type AuthTokenIndexes struct { @@ -19,11 +18,7 @@ type AuthTokenIndexes struct { } func (mig *AuthTokenIndexes) Execute(ctx context.Context, _ eventstore.Event) error { - stmt, err := readStmt(tokenIndexes08, "08", mig.dbClient.Type(), "08.sql") - if err != nil { - return err - } - _, err = mig.dbClient.ExecContext(ctx, stmt) + _, err := mig.dbClient.ExecContext(ctx, tokenIndexes08) return err } diff --git a/cmd/setup/08/postgres/08.sql b/cmd/setup/08/08.sql similarity index 100% rename from cmd/setup/08/postgres/08.sql rename to cmd/setup/08/08.sql diff --git a/cmd/setup/08/cockroach/08.sql b/cmd/setup/08/cockroach/08.sql deleted file mode 100644 index aec4d54303..0000000000 --- a/cmd/setup/08/cockroach/08.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE INDEX IF NOT EXISTS inst_refresh_tkn_idx ON auth.tokens(instance_id, refresh_token_id); -CREATE INDEX IF NOT EXISTS inst_app_tkn_idx ON auth.tokens(instance_id, application_id); -CREATE INDEX IF NOT EXISTS inst_ro_tkn_idx ON auth.tokens(instance_id, resource_owner); -DROP INDEX IF EXISTS auth.tokens@user_user_agent_idx; -CREATE INDEX IF NOT EXISTS inst_usr_agnt_tkn_idx ON auth.tokens(instance_id, user_id, user_agent_id); \ No newline at end of file diff --git a/cmd/setup/10.go b/cmd/setup/10.go index 93c017305c..b134fcab62 100644 --- a/cmd/setup/10.go +++ b/cmd/setup/10.go @@ -3,7 +3,7 @@ package setup import ( "context" "database/sql" - "embed" + _ "embed" "time" "github.com/cockroachdb/cockroach-go/v2/crdb" @@ -18,9 +18,8 @@ var ( correctCreationDate10CreateTable string //go:embed 10/10_fill_table.sql correctCreationDate10FillTable string - //go:embed 10/cockroach/10_update.sql - //go:embed 10/postgres/10_update.sql - correctCreationDate10Update embed.FS + //go:embed 10/10_update.sql + correctCreationDate10Update string //go:embed 10/10_count_wrong_events.sql correctCreationDate10CountWrongEvents string //go:embed 10/10_empty_table.sql @@ -40,11 +39,6 @@ func (mig *CorrectCreationDate) Execute(ctx context.Context, _ eventstore.Event) logging.WithFields("mig", mig.String(), "iteration", i).Debug("start iteration") var affected int64 err = crdb.ExecuteTx(ctx, mig.dbClient.DB, nil, func(tx *sql.Tx) error { - if mig.dbClient.Type() == "cockroach" { - if _, err := tx.Exec("SET experimental_enable_temp_tables=on"); err != nil { - return err - } - } _, err := tx.ExecContext(ctx, correctCreationDate10CreateTable) if err != nil { return err @@ -66,11 +60,7 @@ func (mig *CorrectCreationDate) Execute(ctx context.Context, _ eventstore.Event) return err } - updateStmt, err := readStmt(correctCreationDate10Update, "10", mig.dbClient.Type(), "10_update.sql") - if err != nil { - return err - } - _, err = tx.ExecContext(ctx, updateStmt) + _, err = tx.ExecContext(ctx, correctCreationDate10Update) if err != nil { return err } diff --git a/cmd/setup/10/postgres/10_update.sql b/cmd/setup/10/10_update.sql similarity index 100% rename from cmd/setup/10/postgres/10_update.sql rename to cmd/setup/10/10_update.sql diff --git a/cmd/setup/10/cockroach/10_update.sql b/cmd/setup/10/cockroach/10_update.sql deleted file mode 100644 index 9e7d7f993a..0000000000 --- a/cmd/setup/10/cockroach/10_update.sql +++ /dev/null @@ -1 +0,0 @@ -UPDATE eventstore.events e SET (creation_date, "position") = (we.next_cd, we.next_cd::DECIMAL) FROM wrong_events we WHERE e.event_sequence = we.event_sequence AND e.instance_id = we.instance_id; diff --git a/cmd/setup/14.go b/cmd/setup/14.go index f0ea1b819a..2cd5ac2c57 100644 --- a/cmd/setup/14.go +++ b/cmd/setup/14.go @@ -15,8 +15,7 @@ import ( ) var ( - //go:embed 14/cockroach/*.sql - //go:embed 14/postgres/*.sql + //go:embed 14/*.sql newEventsTable embed.FS ) @@ -40,7 +39,7 @@ func (mig *NewEventsTable) Execute(ctx context.Context, _ eventstore.Event) erro return err } - statements, err := readStatements(newEventsTable, "14", mig.dbClient.Type()) + statements, err := readStatements(newEventsTable, "14") if err != nil { return err } diff --git a/cmd/setup/14/cockroach/01_disable_inserts.sql b/cmd/setup/14/01_disable_inserts.sql similarity index 100% rename from cmd/setup/14/cockroach/01_disable_inserts.sql rename to cmd/setup/14/01_disable_inserts.sql diff --git a/cmd/setup/14/postgres/02_create_and_fill_events2.sql b/cmd/setup/14/02_create_and_fill_events2.sql similarity index 100% rename from cmd/setup/14/postgres/02_create_and_fill_events2.sql rename to cmd/setup/14/02_create_and_fill_events2.sql diff --git a/cmd/setup/14/postgres/03_events2_pk.sql b/cmd/setup/14/03_events2_pk.sql similarity index 100% rename from cmd/setup/14/postgres/03_events2_pk.sql rename to cmd/setup/14/03_events2_pk.sql diff --git a/cmd/setup/14/postgres/04_constraints.sql b/cmd/setup/14/04_constraints.sql similarity index 100% rename from cmd/setup/14/postgres/04_constraints.sql rename to cmd/setup/14/04_constraints.sql diff --git a/cmd/setup/14/postgres/05_indexes.sql b/cmd/setup/14/05_indexes.sql similarity index 100% rename from cmd/setup/14/postgres/05_indexes.sql rename to cmd/setup/14/05_indexes.sql diff --git a/cmd/setup/14/cockroach/02_create_and_fill_events2.sql b/cmd/setup/14/cockroach/02_create_and_fill_events2.sql deleted file mode 100644 index 300ac4b621..0000000000 --- a/cmd/setup/14/cockroach/02_create_and_fill_events2.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE TABLE eventstore.events2 ( - instance_id, - aggregate_type, - aggregate_id, - - event_type, - "sequence", - revision, - created_at, - payload, - creator, - "owner", - - "position", - in_tx_order, - - PRIMARY KEY (instance_id, aggregate_type, aggregate_id, "sequence") -) AS SELECT - instance_id, - aggregate_type, - aggregate_id, - - event_type, - event_sequence, - substr(aggregate_version, 2)::SMALLINT, - creation_date, - event_data, - editor_user, - resource_owner, - - creation_date::DECIMAL, - event_sequence -FROM eventstore.events_old; \ No newline at end of file diff --git a/cmd/setup/14/cockroach/03_constraints.sql b/cmd/setup/14/cockroach/03_constraints.sql deleted file mode 100644 index 62f119cc43..0000000000 --- a/cmd/setup/14/cockroach/03_constraints.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE eventstore.events2 ALTER COLUMN event_type SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN revision SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN creator SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN "owner" SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN "position" SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN in_tx_order SET NOT NULL; \ No newline at end of file diff --git a/cmd/setup/14/cockroach/04_indexes.sql b/cmd/setup/14/cockroach/04_indexes.sql deleted file mode 100644 index a442653606..0000000000 --- a/cmd/setup/14/cockroach/04_indexes.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE INDEX IF NOT EXISTS es_active_instances ON eventstore.events2 (created_at DESC) STORING ("position"); -CREATE INDEX IF NOT EXISTS es_wm ON eventstore.events2 (aggregate_id, instance_id, aggregate_type, event_type); -CREATE INDEX IF NOT EXISTS es_projection ON eventstore.events2 (instance_id, aggregate_type, event_type, "position"); \ No newline at end of file diff --git a/cmd/setup/14/postgres/01_disable_inserts.sql b/cmd/setup/14/postgres/01_disable_inserts.sql deleted file mode 100644 index 0f3c277eba..0000000000 --- a/cmd/setup/14/postgres/01_disable_inserts.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE eventstore.events RENAME TO events_old; \ No newline at end of file diff --git a/cmd/setup/15.go b/cmd/setup/15.go index 2e75ffb118..54161ddef9 100644 --- a/cmd/setup/15.go +++ b/cmd/setup/15.go @@ -11,8 +11,7 @@ import ( ) var ( - //go:embed 15/cockroach/*.sql - //go:embed 15/postgres/*.sql + //go:embed 15/*.sql currentProjectionState embed.FS ) @@ -21,7 +20,7 @@ type CurrentProjectionState struct { } func (mig *CurrentProjectionState) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(currentProjectionState, "15", mig.dbClient.Type()) + statements, err := readStatements(currentProjectionState, "15") if err != nil { return err } diff --git a/cmd/setup/15/cockroach/01_new_failed_events.sql b/cmd/setup/15/01_new_failed_events.sql similarity index 100% rename from cmd/setup/15/cockroach/01_new_failed_events.sql rename to cmd/setup/15/01_new_failed_events.sql diff --git a/cmd/setup/15/postgres/02_fe_from_projections.sql b/cmd/setup/15/02_fe_from_projections.sql similarity index 100% rename from cmd/setup/15/postgres/02_fe_from_projections.sql rename to cmd/setup/15/02_fe_from_projections.sql diff --git a/cmd/setup/15/cockroach/03_fe_from_adminapi.sql b/cmd/setup/15/03_fe_from_adminapi.sql similarity index 100% rename from cmd/setup/15/cockroach/03_fe_from_adminapi.sql rename to cmd/setup/15/03_fe_from_adminapi.sql diff --git a/cmd/setup/15/cockroach/04_fe_from_auth.sql b/cmd/setup/15/04_fe_from_auth.sql similarity index 100% rename from cmd/setup/15/cockroach/04_fe_from_auth.sql rename to cmd/setup/15/04_fe_from_auth.sql diff --git a/cmd/setup/15/cockroach/05_current_states.sql b/cmd/setup/15/05_current_states.sql similarity index 100% rename from cmd/setup/15/cockroach/05_current_states.sql rename to cmd/setup/15/05_current_states.sql diff --git a/cmd/setup/15/postgres/06_cs_from_projections.sql b/cmd/setup/15/06_cs_from_projections.sql similarity index 100% rename from cmd/setup/15/postgres/06_cs_from_projections.sql rename to cmd/setup/15/06_cs_from_projections.sql diff --git a/cmd/setup/15/postgres/07_cs_from_adminapi.sql b/cmd/setup/15/07_cs_from_adminapi.sql similarity index 100% rename from cmd/setup/15/postgres/07_cs_from_adminapi.sql rename to cmd/setup/15/07_cs_from_adminapi.sql diff --git a/cmd/setup/15/postgres/08_cs_from_auth.sql b/cmd/setup/15/08_cs_from_auth.sql similarity index 100% rename from cmd/setup/15/postgres/08_cs_from_auth.sql rename to cmd/setup/15/08_cs_from_auth.sql diff --git a/cmd/setup/15/cockroach/02_fe_from_projections.sql b/cmd/setup/15/cockroach/02_fe_from_projections.sql deleted file mode 100644 index 8bf7a4b8d4..0000000000 --- a/cmd/setup/15/cockroach/02_fe_from_projections.sql +++ /dev/null @@ -1,26 +0,0 @@ -INSERT INTO projections.failed_events2 ( - projection_name - , instance_id - , aggregate_type - , aggregate_id - , event_creation_date - , failed_sequence - , failure_count - , error - , last_failed -) SELECT - fe.projection_name - , fe.instance_id - , e.aggregate_type - , e.aggregate_id - , e.created_at - , e.sequence - , fe.failure_count - , fe.error - , fe.last_failed -FROM - projections.failed_events fe -JOIN eventstore.events2 e ON - e.instance_id = fe.instance_id - AND e.sequence = fe.failed_sequence -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/cockroach/06_cs_from_projections.sql b/cmd/setup/15/cockroach/06_cs_from_projections.sql deleted file mode 100644 index 579afb6d4c..0000000000 --- a/cmd/setup/15/cockroach/06_cs_from_projections.sql +++ /dev/null @@ -1,29 +0,0 @@ -INSERT INTO projections.current_states ( - projection_name - , instance_id - , event_date - , "position" - , last_updated -) (SELECT - cs.projection_name - , cs.instance_id - , e.created_at - , e.position - , cs.timestamp -FROM - projections.current_sequences cs -JOIN eventstore.events2 e ON - e.instance_id = cs.instance_id - AND e.aggregate_type = cs.aggregate_type - AND e.sequence = cs.current_sequence - AND cs.current_sequence = ( - SELECT - MAX(cs2.current_sequence) - FROM - projections.current_sequences cs2 - WHERE - cs.projection_name = cs2.projection_name - AND cs.instance_id = cs2.instance_id - ) -) -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/cockroach/07_cs_from_adminapi.sql b/cmd/setup/15/cockroach/07_cs_from_adminapi.sql deleted file mode 100644 index c40d13a067..0000000000 --- a/cmd/setup/15/cockroach/07_cs_from_adminapi.sql +++ /dev/null @@ -1,28 +0,0 @@ -INSERT INTO projections.current_states ( - projection_name - , instance_id - , event_date - , "position" - , last_updated -) (SELECT - cs.view_name - , cs.instance_id - , e.created_at - , e.position - , cs.last_successful_spooler_run -FROM - adminapi.current_sequences cs -JOIN eventstore.events2 e ON - e.instance_id = cs.instance_id - AND e.sequence = cs.current_sequence - AND cs.current_sequence = ( - SELECT - MAX(cs2.current_sequence) - FROM - adminapi.current_sequences cs2 - WHERE - cs.view_name = cs2.view_name - AND cs.instance_id = cs2.instance_id - ) -) -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/cockroach/08_cs_from_auth.sql b/cmd/setup/15/cockroach/08_cs_from_auth.sql deleted file mode 100644 index c8e7236107..0000000000 --- a/cmd/setup/15/cockroach/08_cs_from_auth.sql +++ /dev/null @@ -1,28 +0,0 @@ -INSERT INTO projections.current_states ( - projection_name - , instance_id - , event_date - , "position" - , last_updated -) (SELECT - cs.view_name - , cs.instance_id - , e.created_at - , e.position - , cs.last_successful_spooler_run -FROM - auth.current_sequences cs -JOIN eventstore.events2 e ON - e.instance_id = cs.instance_id - AND e.sequence = cs.current_sequence - AND cs.current_sequence = ( - SELECT - MAX(cs2.current_sequence) - FROM - auth.current_sequences cs2 - WHERE - cs.view_name = cs2.view_name - AND cs.instance_id = cs2.instance_id - ) -) -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/postgres/01_new_failed_events.sql b/cmd/setup/15/postgres/01_new_failed_events.sql deleted file mode 100644 index 5fa39c08a5..0000000000 --- a/cmd/setup/15/postgres/01_new_failed_events.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS projections.failed_events2 ( - projection_name TEXT NOT NULL - , instance_id TEXT NOT NULL - - , aggregate_type TEXT NOT NULL - , aggregate_id TEXT NOT NULL - , event_creation_date TIMESTAMPTZ NOT NULL - , failed_sequence INT8 NOT NULL - - , failure_count INT2 NULL DEFAULT 0 - , error TEXT - , last_failed TIMESTAMPTZ - - , PRIMARY KEY (projection_name, instance_id, aggregate_type, aggregate_id, failed_sequence) -); -CREATE INDEX IF NOT EXISTS fe2_instance_id_idx on projections.failed_events2 (instance_id); \ No newline at end of file diff --git a/cmd/setup/15/postgres/03_fe_from_adminapi.sql b/cmd/setup/15/postgres/03_fe_from_adminapi.sql deleted file mode 100644 index 1616662fed..0000000000 --- a/cmd/setup/15/postgres/03_fe_from_adminapi.sql +++ /dev/null @@ -1,26 +0,0 @@ -INSERT INTO projections.failed_events2 ( - projection_name - , instance_id - , aggregate_type - , aggregate_id - , event_creation_date - , failed_sequence - , failure_count - , error - , last_failed -) SELECT - fe.view_name - , fe.instance_id - , e.aggregate_type - , e.aggregate_id - , e.created_at - , e.sequence - , fe.failure_count - , fe.err_msg - , fe.last_failed -FROM - adminapi.failed_events fe -JOIN eventstore.events2 e ON - e.instance_id = fe.instance_id - AND e.sequence = fe.failed_sequence -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/postgres/04_fe_from_auth.sql b/cmd/setup/15/postgres/04_fe_from_auth.sql deleted file mode 100644 index a249293e24..0000000000 --- a/cmd/setup/15/postgres/04_fe_from_auth.sql +++ /dev/null @@ -1,26 +0,0 @@ -INSERT INTO projections.failed_events2 ( - projection_name - , instance_id - , aggregate_type - , aggregate_id - , event_creation_date - , failed_sequence - , failure_count - , error - , last_failed -) SELECT - fe.view_name - , fe.instance_id - , e.aggregate_type - , e.aggregate_id - , e.created_at - , e.sequence - , fe.failure_count - , fe.err_msg - , fe.last_failed -FROM - auth.failed_events fe -JOIN eventstore.events2 e ON - e.instance_id = fe.instance_id - AND e.sequence = fe.failed_sequence -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/postgres/05_current_states.sql b/cmd/setup/15/postgres/05_current_states.sql deleted file mode 100644 index bc2f5ed771..0000000000 --- a/cmd/setup/15/postgres/05_current_states.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE IF NOT EXISTS projections.current_states ( - projection_name TEXT NOT NULL - , instance_id TEXT NOT NULL - - , last_updated TIMESTAMPTZ - - , aggregate_id TEXT - , aggregate_type TEXT - , "sequence" INT8 - , event_date TIMESTAMPTZ - , "position" DECIMAL - - , PRIMARY KEY (projection_name, instance_id) -); -CREATE INDEX IF NOT EXISTS cs_instance_id_idx ON projections.current_states (instance_id); \ No newline at end of file diff --git a/cmd/setup/34.go b/cmd/setup/34.go index 59854e9e97..75e4076803 100644 --- a/cmd/setup/34.go +++ b/cmd/setup/34.go @@ -3,17 +3,14 @@ package setup import ( "context" _ "embed" - "fmt" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" ) var ( - //go:embed 34/cockroach/34_cache_schema.sql - addCacheSchemaCockroach string - //go:embed 34/postgres/34_cache_schema.sql - addCacheSchemaPostgres string + //go:embed 34/34_cache_schema.sql + addCacheSchema string ) type AddCacheSchema struct { @@ -21,14 +18,7 @@ type AddCacheSchema struct { } func (mig *AddCacheSchema) Execute(ctx context.Context, _ eventstore.Event) (err error) { - switch mig.dbClient.Type() { - case "cockroach": - _, err = mig.dbClient.ExecContext(ctx, addCacheSchemaCockroach) - case "postgres": - _, err = mig.dbClient.ExecContext(ctx, addCacheSchemaPostgres) - default: - err = fmt.Errorf("add cache schema: unsupported db type %q", mig.dbClient.Type()) - } + _, err = mig.dbClient.ExecContext(ctx, addCacheSchema) return err } diff --git a/cmd/setup/34/postgres/34_cache_schema.sql b/cmd/setup/34/34_cache_schema.sql similarity index 100% rename from cmd/setup/34/postgres/34_cache_schema.sql rename to cmd/setup/34/34_cache_schema.sql diff --git a/cmd/setup/34/cockroach/34_cache_schema.sql b/cmd/setup/34/cockroach/34_cache_schema.sql deleted file mode 100644 index 0f866b0ccd..0000000000 --- a/cmd/setup/34/cockroach/34_cache_schema.sql +++ /dev/null @@ -1,27 +0,0 @@ -create schema if not exists cache; - -create table if not exists cache.objects ( - cache_name varchar not null, - id uuid not null default gen_random_uuid(), - created_at timestamptz not null default now(), - last_used_at timestamptz not null default now(), - payload jsonb not null, - - primary key(cache_name, id) -); - -create table if not exists cache.string_keys( - cache_name varchar not null check (cache_name <> ''), - index_id integer not null check (index_id > 0), - index_key varchar not null check (index_key <> ''), - object_id uuid not null, - - primary key (cache_name, index_id, index_key), - constraint fk_object - foreign key(cache_name, object_id) - references cache.objects(cache_name, id) - on delete cascade -); - -create index if not exists string_keys_object_id_idx - on cache.string_keys (cache_name, object_id); -- for delete cascade diff --git a/cmd/setup/35.go b/cmd/setup/35.go index f8473cfbfd..68e08bdfdb 100644 --- a/cmd/setup/35.go +++ b/cmd/setup/35.go @@ -21,7 +21,7 @@ type AddPositionToIndexEsWm struct { } func (mig *AddPositionToIndexEsWm) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(addPositionToEsWmIndex, "35", "") + statements, err := readStatements(addPositionToEsWmIndex, "35") if err != nil { return err } diff --git a/cmd/setup/40.go b/cmd/setup/40.go index b16b9226f7..86cdab0d11 100644 --- a/cmd/setup/40.go +++ b/cmd/setup/40.go @@ -24,8 +24,7 @@ const ( ) var ( - //go:embed 40/cockroach/*.sql - //go:embed 40/postgres/*.sql + //go:embed 40/*.sql initPushFunc embed.FS ) @@ -112,5 +111,5 @@ func (mig *InitPushFunc) inTxOrderType(ctx context.Context) (typeName string, er } func (mig *InitPushFunc) filePath(fileName string) string { - return path.Join("40", mig.dbClient.Type(), fileName) + return path.Join("40", fileName) } diff --git a/cmd/setup/40/cockroach/00_in_tx_order_type.sql b/cmd/setup/40/00_in_tx_order_type.sql similarity index 100% rename from cmd/setup/40/cockroach/00_in_tx_order_type.sql rename to cmd/setup/40/00_in_tx_order_type.sql diff --git a/cmd/setup/40/postgres/01_type.sql b/cmd/setup/40/01_type.sql similarity index 100% rename from cmd/setup/40/postgres/01_type.sql rename to cmd/setup/40/01_type.sql diff --git a/cmd/setup/40/postgres/02_func.sql b/cmd/setup/40/02_func.sql similarity index 100% rename from cmd/setup/40/postgres/02_func.sql rename to cmd/setup/40/02_func.sql diff --git a/cmd/setup/40/cockroach/01_type.sql b/cmd/setup/40/cockroach/01_type.sql deleted file mode 100644 index e26af2f828..0000000000 --- a/cmd/setup/40/cockroach/01_type.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TYPE IF NOT EXISTS eventstore.command AS ( - instance_id TEXT - , aggregate_type TEXT - , aggregate_id TEXT - , command_type TEXT - , revision INT2 - , payload JSONB - , creator TEXT - , owner TEXT -); diff --git a/cmd/setup/40/cockroach/02_func.sql b/cmd/setup/40/cockroach/02_func.sql deleted file mode 100644 index 9cb45529ad..0000000000 --- a/cmd/setup/40/cockroach/02_func.sql +++ /dev/null @@ -1,137 +0,0 @@ -CREATE OR REPLACE FUNCTION eventstore.latest_aggregate_state( - instance_id TEXT - , aggregate_type TEXT - , aggregate_id TEXT - - , sequence OUT BIGINT - , owner OUT TEXT -) - LANGUAGE 'plpgsql' -AS $$ - BEGIN - SELECT - COALESCE(e.sequence, 0) AS sequence - , e.owner - INTO - sequence - , owner - FROM - eventstore.events2 e - WHERE - e.instance_id = $1 - AND e.aggregate_type = $2 - AND e.aggregate_id = $3 - ORDER BY - e.sequence DESC - LIMIT 1; - - RETURN; - END; -$$; - -CREATE OR REPLACE FUNCTION eventstore.commands_to_events2(commands eventstore.command[]) - RETURNS eventstore.events2[] - LANGUAGE 'plpgsql' -AS $$ -DECLARE - current_sequence BIGINT; - current_owner TEXT; - - instance_id TEXT; - aggregate_type TEXT; - aggregate_id TEXT; - - _events eventstore.events2[]; - - _aggregates CURSOR FOR - select - DISTINCT ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - FROM - UNNEST(commands) AS c; -BEGIN - OPEN _aggregates; - LOOP - FETCH NEXT IN _aggregates INTO instance_id, aggregate_type, aggregate_id; - -- crdb does not support EXIT WHEN NOT FOUND - EXIT WHEN instance_id IS NULL; - - SELECT - * - INTO - current_sequence - , current_owner - FROM eventstore.latest_aggregate_state( - instance_id - , aggregate_type - , aggregate_id - ); - - -- RETURN QUERY is not supported by crdb: https://github.com/cockroachdb/cockroach/issues/105240 - SELECT - ARRAY_CAT(_events, ARRAY_AGG(e)) - INTO - _events - FROM ( - SELECT - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - , ("c").command_type -- AS event_type - , COALESCE(current_sequence, 0) + ROW_NUMBER() OVER () -- AS sequence - , ("c").revision - , NOW() -- AS created_at - , ("c").payload - , ("c").creator - , COALESCE(current_owner, ("c").owner) -- AS owner - , cluster_logical_timestamp() -- AS position - , ordinality::{{ .InTxOrderType }} -- AS in_tx_order - FROM - UNNEST(commands) WITH ORDINALITY AS c - WHERE - ("c").instance_id = instance_id - AND ("c").aggregate_type = aggregate_type - AND ("c").aggregate_id = aggregate_id - ) AS e; - END LOOP; - CLOSE _aggregates; - RETURN _events; -END; -$$; - -CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command[]) RETURNS SETOF eventstore.events2 AS $$ - INSERT INTO eventstore.events2 - SELECT - ("e").instance_id - , ("e").aggregate_type - , ("e").aggregate_id - , ("e").event_type - , ("e").sequence - , ("e").revision - , ("e").created_at - , ("e").payload - , ("e").creator - , ("e").owner - , ("e")."position" - , ("e").in_tx_order - FROM - UNNEST(eventstore.commands_to_events2(commands)) e - ORDER BY - in_tx_order - RETURNING * -$$ LANGUAGE SQL; - -/* -select (c).* from UNNEST(eventstore.commands_to_events2( -ARRAY[ - ROW('', 'system', 'SYSTEM', 'ct1', 1, '{"key": "value"}', 'c1', 'SYSTEM') - , ROW('', 'system', 'SYSTEM', 'ct2', 1, '{"key": "value"}', 'c1', 'SYSTEM') - , ROW('289525561255060732', 'org', '289575074711790844', 'ct3', 1, '{"key": "value"}', 'c1', '289575074711790844') - , ROW('289525561255060732', 'user', '289575075164906748', 'ct3', 1, '{"key": "value"}', 'c1', '289575074711790844') - , ROW('289525561255060732', 'oidc_session', 'V2_289575178579535100', 'ct3', 1, '{"key": "value"}', 'c1', '289575074711790844') - , ROW('', 'system', 'SYSTEM', 'ct3', 1, '{"key": "value"}', 'c1', 'SYSTEM') -]::eventstore.command[] -) )c; -*/ - diff --git a/cmd/setup/40/postgres/00_in_tx_order_type.sql b/cmd/setup/40/postgres/00_in_tx_order_type.sql deleted file mode 100644 index 68b7daf984..0000000000 --- a/cmd/setup/40/postgres/00_in_tx_order_type.sql +++ /dev/null @@ -1,5 +0,0 @@ -SELECT data_type -FROM information_schema.columns -WHERE table_schema = 'eventstore' -AND table_name = 'events2' -AND column_name = 'in_tx_order'; diff --git a/cmd/setup/43.go b/cmd/setup/43.go index 844c25cf24..1fa09773bc 100644 --- a/cmd/setup/43.go +++ b/cmd/setup/43.go @@ -12,8 +12,7 @@ import ( ) var ( - //go:embed 43/cockroach/*.sql - //go:embed 43/postgres/*.sql + //go:embed 43/*.sql createFieldsDomainIndex embed.FS ) @@ -22,7 +21,7 @@ type CreateFieldsDomainIndex struct { } func (mig *CreateFieldsDomainIndex) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(createFieldsDomainIndex, "43", mig.dbClient.Type()) + statements, err := readStatements(createFieldsDomainIndex, "43") if err != nil { return err } diff --git a/cmd/setup/43/postgres/43.sql b/cmd/setup/43/43.sql similarity index 100% rename from cmd/setup/43/postgres/43.sql rename to cmd/setup/43/43.sql diff --git a/cmd/setup/43/cockroach/43.sql b/cmd/setup/43/cockroach/43.sql deleted file mode 100644 index 9152130970..0000000000 --- a/cmd/setup/43/cockroach/43.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE INDEX CONCURRENTLY IF NOT EXISTS fields_instance_domains_idx -ON eventstore.fields (object_id) -WHERE object_type = 'instance_domain' AND field_name = 'domain'; \ No newline at end of file diff --git a/cmd/setup/44.go b/cmd/setup/44.go index 11c355a053..5eb2f8d5c1 100644 --- a/cmd/setup/44.go +++ b/cmd/setup/44.go @@ -21,7 +21,7 @@ type ReplaceCurrentSequencesIndex struct { } func (mig *ReplaceCurrentSequencesIndex) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(replaceCurrentSequencesIndex, "44", "") + statements, err := readStatements(replaceCurrentSequencesIndex, "44") if err != nil { return err } diff --git a/cmd/setup/46.go b/cmd/setup/46.go index e48b16e4b0..3593a1b668 100644 --- a/cmd/setup/46.go +++ b/cmd/setup/46.go @@ -21,7 +21,7 @@ var ( ) func (mig *InitPermissionFunctions) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(permissionFunctions, "46", "") + statements, err := readStatements(permissionFunctions, "46") if err != nil { return err } diff --git a/cmd/setup/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 943ac164ea..e0a07c0a9d 100644 --- a/cmd/setup/cleanup.go +++ b/cmd/setup/cleanup.go @@ -35,7 +35,7 @@ func Cleanup(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 f13802ffa0..fe628c8df2 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -55,7 +55,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") @@ -107,7 +107,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 @@ -137,7 +137,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} @@ -179,6 +179,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") @@ -220,6 +221,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s50IDPTemplate6UsePKCE, steps.s51IDPTemplate6RootCA, steps.s52IDPTemplate6LDAP2, + steps.s53InitPermittedOrgsFunction, } { mustExecuteMigration(ctx, eventstoreClient, step, "migration failed") } @@ -308,8 +310,8 @@ func mustExecuteMigration(ctx context.Context, eventstoreClient *eventstore.Even // 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 } @@ -322,16 +324,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 } @@ -412,7 +413,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 @@ -437,7 +438,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, @@ -472,9 +473,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, }) @@ -501,7 +499,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 910759b653..e973c40479 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 76ffdb8921..e3d84625b4 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -4,7 +4,6 @@ import ( "context" "crypto/tls" _ "embed" - "errors" "fmt" "math" "net/http" @@ -35,6 +34,7 @@ import ( "github.com/zitadel/zitadel/internal/api" "github.com/zitadel/zitadel/internal/api/assets" internal_authz "github.com/zitadel/zitadel/internal/api/authz" + action_v2_beta "github.com/zitadel/zitadel/internal/api/grpc/action/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/admin" "github.com/zitadel/zitadel/internal/api/grpc/auth" feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2" @@ -45,11 +45,9 @@ import ( oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta" org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta" - action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/action/v3alpha" "github.com/zitadel/zitadel/internal/api/grpc/resources/debug_events/debug_events" user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha" userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha" - "github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3" saml_v2 "github.com/zitadel/zitadel/internal/api/grpc/saml/v2" session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta" @@ -58,6 +56,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/system" user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" + webkey "github.com/zitadel/zitadel/internal/api/grpc/webkey/v2beta" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/idp" @@ -82,13 +81,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 +107,7 @@ func New(server chan<- *Server) *cobra.Command { Short: "starts ZITADEL instance", Long: `starts ZITADEL. Requirements: -- cockroachdb`, +- postgreSQL`, RunE: func(cmd *cobra.Command, args []string) error { err := cmd_tls.ModeFromFlag(cmd) if err != nil { @@ -163,7 +163,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server config.Eventstore.Pusher = new_es.NewEventstore(dbClient) config.Eventstore.Searcher = new_es.NewEventstore(dbClient) - config.Eventstore.Querier = old_es.NewCRDB(dbClient) + config.Eventstore.Querier = old_es.NewPostgres(dbClient) eventstoreClient := eventstore.NewEventstore(config.Eventstore) eventstoreV4 := es_v4.NewEventstoreFromOne(es_v4_pg.New(dbClient, &es_v4_pg.Config{ MaxRetries: config.Eventstore.MaxRetries, @@ -193,7 +193,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, config.AuditLogRetention, @@ -209,7 +209,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server return fmt.Errorf("error starting authz repo: %w", err) } permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } storage, err := config.AssetStorage.NewStorage(dbClient.DB) @@ -257,11 +257,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 } @@ -269,9 +270,6 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server actionsLogstoreSvc := logstore.New(queries, actionsExecutionDBEmitter, actionsExecutionStdoutEmitter) actions.SetLogstoreService(actionsLogstoreSvc) - if !config.Notifications.LegacyEnabled && dbClient.Type() == "cockroach" { - return errors.New("notifications must be set to LegacyEnabled=true when using CockroachDB") - } q, err := queue.NewQueue(&queue.Config{ Client: dbClient, }) @@ -300,11 +298,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 +402,23 @@ func startAPIs( return nil, err } - accessStdoutEmitter, err := logstore.NewEmitter[*record.AccessLog](ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Access.Stdout.Enabled}, stdout.NewStdoutEmitter[*record.AccessLog]()) + accessStdoutEmitter, err := logstore.NewEmitter(ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Access.Stdout.Enabled}, emit_stdout.NewStdoutEmitter[*record.AccessLog]()) if err != nil { return nil, err } - accessDBEmitter, err := logstore.NewEmitter[*record.AccessLog](ctx, clock, &config.Quotas.Access.EmitterConfig, access.NewDatabaseLogStorage(dbClient, commands, queries)) + accessDBEmitter, err := logstore.NewEmitter(ctx, clock, &config.Quotas.Access.EmitterConfig, access.NewDatabaseLogStorage(dbClient, commands, queries)) if err != nil { return nil, err } - accessSvc := logstore.New[*record.AccessLog](queries, accessDBEmitter, accessStdoutEmitter) + accessSvc := logstore.New(queries, accessDBEmitter, accessStdoutEmitter) exhaustedCookieHandler := http_util.NewCookieHandler( http_util.WithUnsecure(), http_util.WithNonHttpOnly(), http_util.WithMaxAge(int(math.Floor(config.Quotas.Access.ExhaustedCookieMaxAge.Seconds()))), ) limitingAccessInterceptor := middleware.NewAccessInterceptor(accessSvc, exhaustedCookieHandler, &config.Quotas.Access.AccessConfig) - apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.ExternalDomain, append(config.InstanceHostHeaders, config.PublicHostHeaders...), limitingAccessInterceptor) + apis, err := api.New(ctx, config.Port, router, queries, verifier, config.SystemAuthZ, config.InternalAuthZ, tlsConfig, config.ExternalDomain, append(config.InstanceHostHeaders, config.PublicHostHeaders...), limitingAccessInterceptor) if err != nil { return nil, fmt.Errorf("error creating api %w", err) } @@ -477,7 +484,7 @@ func startAPIs( if err := apis.RegisterService(ctx, idp_v2.CreateServer(commands, queries, permissionCheck)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(config.SystemDefaults, commands, queries, domain.AllActionFunctions, 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 +501,7 @@ func startAPIs( } instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...) assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) - apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) + apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.SystemAuthZ, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler)) @@ -538,7 +545,7 @@ func startAPIs( keys.User, &config.SCIM, instanceInterceptor.HandlerFuncWithError, - middleware.AuthorizationInterceptor(verifier, config.InternalAuthZ).HandlerFuncWithError)) + middleware.AuthorizationInterceptor(verifier, config.SystemAuthZ, config.InternalAuthZ).HandlerFuncWithError)) c, err := console.Start(config.Console, config.ExternalSecure, oidcServer.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, limitingAccessInterceptor, config.CustomerPortal) if err != nil { @@ -604,7 +611,7 @@ func listen(ctx context.Context, router *mux.Router, port uint16, tlsConfig *tls go func() { logging.Infof("server is listening on %s", lis.Addr().String()) if tlsConfig != nil { - //we don't need to pass the files here, because we already initialized the TLS config on the server + // we don't need to pass the files here, because we already initialized the TLS config on the server errCh <- http1Server.ServeTLS(lis, "", "") } else { errCh <- http1Server.Serve(lis) diff --git a/cmd/start/start_from_init.go b/cmd/start/start_from_init.go index 38a6a6c4d1..62d705b33c 100644 --- a/cmd/start/start_from_init.go +++ b/cmd/start/start_from_init.go @@ -21,7 +21,7 @@ Second the initial events are created. Last ZITADEL starts. Requirements: -- cockroachdb`, +- postgreSQL`, Run: func(cmd *cobra.Command, args []string) { err := tls.ModeFromFlag(cmd) logging.OnError(err).Fatal("invalid tlsMode") diff --git a/console/package.json b/console/package.json index 8f15250747..2d986730c2 100644 --- a/console/package.json +++ b/console/package.json @@ -32,7 +32,7 @@ "@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/proto": "1.0.5-sha-4118a9d", "angular-oauth2-oidc": "^15.0.1", "angularx-qrcode": "^16.0.2", "buffer": "^6.0.3", @@ -65,6 +65,7 @@ "@angular/compiler-cli": "^16.2.5", "@angular/language-service": "^18.2.4", "@bufbuild/buf": "^1.41.0", + "@netlify/framework-info": "^9.8.13", "@types/file-saver": "^2.0.7", "@types/google-protobuf": "^3.15.3", "@types/jasmine": "~5.1.4", @@ -86,7 +87,6 @@ "karma-jasmine-html-reporter": "^2.1.0", "prettier": "^3.5.3", "prettier-plugin-organize-imports": "^4.1.0", - "typescript": "5.1", - "@netlify/framework-info": "^9.8.13" + "typescript": "5.1" } } diff --git a/console/src/app/components/feature-toggle/feature-toggle.component.html b/console/src/app/components/feature-toggle/feature-toggle.component.html index eb09552445..cb97f1b746 100644 --- a/console/src/app/components/feature-toggle/feature-toggle.component.html +++ b/console/src/app/components/feature-toggle/feature-toggle.component.html @@ -1,29 +1,29 @@ -
- {{ 'SETTING.FEATURES.' + toggleStateKey.toUpperCase() | translate }} +
+ {{ 'SETTING.FEATURES.' + (toggleStateKey | uppercase) | translate }}
- +
{{ 'SETTING.FEATURES.STATES.DISABLED' | translate }}
- +
{{ 'SETTING.FEATURES.STATES.ENABLED' | translate }}
@@ -34,7 +34,7 @@ {{ i18nDescription }}
diff --git a/console/src/app/components/feature-toggle/feature-toggle.component.ts b/console/src/app/components/feature-toggle/feature-toggle.component.ts index 5c09489619..fab0b31d48 100644 --- a/console/src/app/components/feature-toggle/feature-toggle.component.ts +++ b/console/src/app/components/feature-toggle/feature-toggle.component.ts @@ -1,16 +1,14 @@ -import { CommonModule } from '@angular/common'; +import { AsyncPipe, NgIf, UpperCasePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; -import { MatButtonModule } from '@angular/material/button'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { CopyToClipboardModule } from '../../directives/copy-to-clipboard/copy-to-clipboard.module'; -import { CopyRowComponent } from '../copy-row/copy-row.component'; import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module'; -import { ToggleState, ToggleStateKeys, ToggleStates } from '../features/features.component'; +import { ToggleStateKeys, ToggleStates } from '../features/features.component'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { FormsModule } from '@angular/forms'; -import { GetInstanceFeaturesResponse } from '@zitadel/proto/zitadel/feature/v2/instance_pb'; -import { FeatureFlag, Source } from '@zitadel/proto/zitadel/feature/v2/feature_pb'; +import { Source } from '@zitadel/proto/zitadel/feature/v2/feature_pb'; +import { ReplaySubject } from 'rxjs'; +import { map } from 'rxjs/operators'; @Component({ standalone: true, @@ -19,37 +17,29 @@ import { FeatureFlag, Source } from '@zitadel/proto/zitadel/feature/v2/feature_p styleUrls: ['./feature-toggle.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - CommonModule, - FormsModule, - TranslateModule, - MatButtonModule, - InfoSectionModule, - MatTooltipModule, - CopyToClipboardModule, - CopyRowComponent, MatButtonToggleModule, + UpperCasePipe, + TranslateModule, + FormsModule, + MatTooltipModule, + InfoSectionModule, + AsyncPipe, + NgIf, ], }) -export class FeatureToggleComponent { - @Input() featureData: Partial = {}; - @Input() toggleStates: Partial = {}; - @Input() toggleStateKey: string = ''; - @Output() toggleChange = new EventEmitter(); - - protected ToggleState = ToggleState; - protected Source = Source; - - get isInherited(): boolean { - const source = this.featureData[this.toggleStateKey as ToggleStateKeys]?.source; - return source == Source.SYSTEM || source == Source.UNSPECIFIED; +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)); } - get enabled() { - // TODO: remove casting as not all features are a FeatureFlag - return (this.featureData[this.toggleStateKey as ToggleStateKeys] as FeatureFlag)?.enabled; - } + @Output() readonly toggleChange = new EventEmitter(); - onToggleChange() { - this.toggleChange.emit(); - } + 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 95cc889f77..30d8f629af 100644 --- a/console/src/app/components/features/features.component.html +++ b/console/src/app/components/features/features.component.html @@ -13,26 +13,20 @@

{{ 'DESCRIPTIONS.SETTINGS.FEATURES.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 e253bdf023..0f89c5e98a 100644 --- a/console/src/app/components/features/features.component.ts +++ b/console/src/app/components/features/features.component.ts @@ -19,16 +19,14 @@ import { GetInstanceFeaturesResponse, SetInstanceFeaturesRequestSchema, } from '@zitadel/proto/zitadel/feature/v2/instance_pb'; -import { FeatureFlag, Source } from '@zitadel/proto/zitadel/feature/v2/feature_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'; -export enum ToggleState { - ENABLED = 'ENABLED', - DISABLED = 'DISABLED', -} - -// TODO: to add a new feature, add the key here and in the FEATURE_KEYS array -const FEATURE_KEYS: ToggleStateKeys[] = [ +// to add a new feature, add the key here and in the FEATURE_KEYS array +const FEATURE_KEYS = [ 'actions', 'consoleUseV2UserApi', 'debugOidcParentError', @@ -36,23 +34,24 @@ const FEATURE_KEYS: ToggleStateKeys[] = [ 'enableBackChannelLogout', // 'improvedPerformance', 'loginDefaultOrg', - // 'loginV2', 'oidcLegacyIntrospection', 'oidcSingleV1SessionTermination', 'oidcTokenExchange', 'oidcTriggerIntrospectionProjections', 'permissionCheckV2', 'userSchema', - // 'webKey', -]; - -type FeatureState = { source: Source; state: ToggleState }; -export type ToggleStateKeys = Exclude; + 'webKey', +] as const; +export type ToggleState = { source: Source; enabled: boolean }; export type ToggleStates = { - [key in ToggleStateKeys]: FeatureState; + [key in (typeof FEATURE_KEYS)[number]]: ToggleState; +} & { + loginV2: ToggleState & { baseUri: string }; }; +export type ToggleStateKeys = keyof ToggleStates; + @Component({ imports: [ CommonModule, @@ -68,6 +67,7 @@ export type ToggleStates = { MatTooltipModule, HasRoleModule, FeatureToggleComponent, + LoginV2FeatureToggleComponent, ], standalone: true, selector: 'cnsl-features', @@ -75,16 +75,15 @@ export type ToggleStates = { styleUrls: ['./features.component.scss'], }) export class FeaturesComponent { - protected featureData: GetInstanceFeaturesResponse | 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: NewFeatureService, - private breadcrumbService: BreadcrumbService, - private toast: ToastService, + private readonly featureService: NewFeatureService, + private readonly breadcrumbService: BreadcrumbService, + private readonly toast: ToastService, ) { const breadcrumbs = [ new Breadcrumb({ @@ -95,74 +94,84 @@ export class FeaturesComponent { ]; this.breadcrumbService.setBreadcrumb(breadcrumbs); - this.getFeatures(); + this.toggleStates$ = this.getToggleStates().pipe(shareReplay({ refCount: true, bufferSize: 1 })); } - public validateAndSave() { - const req: MessageInitShape = { - actions: this.toggleStates?.actions?.state === ToggleState.ENABLED, - consoleUseV2UserApi: this.toggleStates?.consoleUseV2UserApi?.state === ToggleState.ENABLED, - debugOidcParentError: this.toggleStates?.debugOidcParentError?.state === ToggleState.ENABLED, - disableUserTokenEvent: this.toggleStates?.disableUserTokenEvent?.state === ToggleState.ENABLED, - enableBackChannelLogout: this.toggleStates?.enableBackChannelLogout?.state === ToggleState.ENABLED, - loginDefaultOrg: this.toggleStates?.loginDefaultOrg?.state === ToggleState.ENABLED, - oidcLegacyIntrospection: this.toggleStates?.oidcLegacyIntrospection?.state === ToggleState.ENABLED, - oidcSingleV1SessionTermination: this.toggleStates?.oidcSingleV1SessionTermination?.state === ToggleState.ENABLED, - oidcTokenExchange: this.toggleStates?.oidcTokenExchange?.state === ToggleState.ENABLED, - oidcTriggerIntrospectionProjections: - this.toggleStates?.oidcTriggerIntrospectionProjections?.state === ToggleState.ENABLED, - permissionCheckV2: this.toggleStates?.permissionCheckV2?.state === ToggleState.ENABLED, - userSchema: this.toggleStates?.userSchema?.state === ToggleState.ENABLED, - // webKey: this.toggleStates?.webKey?.state === ToggleState.ENABLED, - }; - - this.featureService - .setInstanceFeatures(req) - .then(() => { - this.toast.showInfo('POLICY.TOAST.SET', true); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - - private getFeatures() { - this.featureService.getInstanceFeatures().then((instanceFeaturesResponse) => { - this.featureData = instanceFeaturesResponse; - this.toggleStates = this.createToggleStates(this.featureData); - }); + 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 createToggleStates(featureData: GetInstanceFeaturesResponse): ToggleStates { - const toggleStates: Partial = {}; - - FEATURE_KEYS.forEach((key) => { - // TODO: Fix this type cast as not all keys are present as FeatureFlag - const feature = featureData[key] as unknown as FeatureFlag; - toggleStates[key] = { - source: feature?.source || Source.SYSTEM, - state: !!feature?.enabled ? ToggleState.ENABLED : ToggleState.DISABLED, - }; - }); - - return toggleStates as 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 ?? '', + }, + } as ToggleStates, + ); } - public resetSettings(): void { - this.featureService - .resetInstanceFeatures() - .then(() => { - this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true); - setTimeout(() => { - this.getFeatures(); - }, 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 get toggleStateKeys() { - return Object.keys(this.toggleStates ?? {}); + 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/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..d91148e862 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,156 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, 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 ?? '', + })), + toArray(), + ); } 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 +159,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..670fe2c53b --- /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,75 @@ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'ACTIONSTWO.EXECUTION.TABLE.CONDITION' | translate }} + + {{ row?.condition | condition }} + + {{ 'ACTIONSTWO.EXECUTION.TABLE.TYPE' | translate }} + {{ 'ACTIONSTWO.EXECUTION.TYPES.' + row?.condition?.conditionType?.case | translate }} + {{ 'ACTIONSTWO.EXECUTION.TABLE.TARGET' | translate }} +
+ {{ target.name }} + + + refresh + {{ condition | condition }} + +
+
+ {{ 'ACTIONSTWO.EXECUTION.TABLE.CREATIONDATE' | translate }} + + {{ row.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..6c714e2908 --- /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,60 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { Observable, ReplaySubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { MatTableDataSource } from '@angular/material/table'; +import { Condition, Execution, ExecutionTargetType } from '@zitadel/proto/zitadel/action/v2beta/execution_pb'; +import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb'; + +@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 delete = new EventEmitter(); + + @Input({ required: true }) + public set executions(executions: Execution[] | null) { + this.executions$.next(executions); + } + + @Input({ required: true }) + public set targets(targets: Target[] | null) { + this.targets$.next(targets); + } + + @Output() + public readonly selected = new EventEmitter(); + + private readonly executions$ = new ReplaySubject(1); + private readonly targets$ = new ReplaySubject(1); + + protected readonly dataSource$ = this.executions$.pipe( + filter(Boolean), + map((keys) => new MatTableDataSource(keys)), + ); + + protected filteredTargetTypes(targets: ExecutionTargetType[]): Observable { + const targetIds = targets + .map((t) => t.type) + .filter((t): t is Extract => t.case === 'target') + .map((t) => t.value); + + return this.targets$.pipe( + filter(Boolean), + map((alltargets) => alltargets!.filter((target) => targetIds.includes(target.id))), + ); + } + + protected filteredIncludeConditions(targets: ExecutionTargetType[]): Condition[] { + return targets + .map((t) => t.type) + .filter((t): t is Extract => t.case === 'include') + .map(({ value }) => value); + } +} 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..c194466a4f --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html @@ -0,0 +1,14 @@ +

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

+

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

+ + + + diff --git a/cmd/initialise/sql/postgres/11_settings.sql b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.scss similarity index 100% rename from cmd/initialise/sql/postgres/11_settings.sql rename to console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.scss diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts new file mode 100644 index 0000000000..6415498837 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts @@ -0,0 +1,143 @@ +import { ChangeDetectionStrategy, Component, DestroyRef, OnInit } from '@angular/core'; +import { ActionService } from 'src/app/services/action.service'; +import { NewFeatureService } from 'src/app/services/new-feature.service'; +import { defer, firstValueFrom, Observable, of, shareReplay, Subject, TimeoutError } from 'rxjs'; +import { catchError, filter, map, startWith, switchMap, tap, timeout } from 'rxjs/operators'; +import { ToastService } from 'src/app/services/toast.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ORGANIZATIONS } from '../../settings-list/settings'; +import { ActionTwoAddActionDialogComponent } from '../actions-two-add-action/actions-two-add-action-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { Execution, ExecutionSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb'; +import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; +import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb'; +import { Value } from 'google-protobuf/google/protobuf/struct_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 implements OnInit { + protected readonly refresh = new Subject(); + private readonly actionsEnabled$: Observable; + protected readonly executions$: Observable; + protected readonly targets$: Observable; + + constructor( + private readonly actionService: ActionService, + private readonly featureService: NewFeatureService, + private readonly toast: ToastService, + private readonly destroyRef: DestroyRef, + private readonly router: Router, + private readonly route: ActivatedRoute, + private readonly dialog: MatDialog, + ) { + this.actionsEnabled$ = this.getActionsEnabled$().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.executions$ = this.getExecutions$(this.actionsEnabled$); + this.targets$ = this.getTargets$(this.actionsEnabled$); + } + + ngOnInit(): void { + // this also preloads + this.actionsEnabled$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async (enabled) => { + if (enabled) { + return; + } + await this.router.navigate([], { + relativeTo: this.route, + queryParams: { + id: ORGANIZATIONS.id, + }, + queryParamsHandling: 'merge', + }); + }); + } + + private getExecutions$(actionsEnabled$: Observable) { + return this.refresh.pipe( + startWith(true), + switchMap(() => { + return this.actionService.listExecutions({}); + }), + map(({ result }) => result), + catchError(async (err) => { + const actionsEnabled = await firstValueFrom(actionsEnabled$); + if (actionsEnabled) { + this.toast.showError(err); + } + return []; + }), + ); + } + + private getTargets$(actionsEnabled$: Observable) { + return this.refresh.pipe( + startWith(true), + switchMap(() => { + return this.actionService.listTargets({}); + }), + map(({ result }) => result), + catchError(async (err) => { + const actionsEnabled = await firstValueFrom(actionsEnabled$); + if (actionsEnabled) { + this.toast.showError(err); + } + return []; + }), + ); + } + + private getActionsEnabled$() { + return defer(() => this.featureService.getInstanceFeatures()).pipe( + map(({ actions }) => actions?.enabled ?? false), + timeout(1000), + catchError((err) => { + if (!(err instanceof TimeoutError)) { + this.toast.showError(err); + } + return of(false); + }), + ); + } + + public openDialog(execution?: Execution): void { + const ref = this.dialog.open(ActionTwoAddActionDialogComponent, { + width: '400px', + data: execution + ? { + execution: execution, + } + : {}, + }); + + ref.afterClosed().subscribe((request?: MessageInitShape) => { + if (request) { + this.actionService + .setExecution(request) + .then(() => { + setTimeout(() => { + this.refresh.next(true); + }, 1000); + }) + .catch((error) => { + console.error(error); + this.toast.showError(error); + }); + } + }); + } + + public async deleteExecution(execution: Execution) { + const deleteReq: MessageInitShape = { + condition: execution.condition, + targets: [], + }; + await this.actionService.setExecution(deleteReq); + await new Promise((res) => setTimeout(res, 1000)); + this.refresh.next(true); + } +} diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html new file mode 100644 index 0000000000..401e5e521d --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html @@ -0,0 +1,116 @@ +
+ +

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

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

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

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

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

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

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

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

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

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

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

+ + + {{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.TARGET.DESCRIPTION' | translate }} + + + + + + {{ target.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..776c535f1a --- /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,12 @@ +.target-description { + margin-bottom: 0; +} + +.actions { + display: flex; + justify-content: space-between; + + .fill-space { + font: 1; + } +} 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..bdcfc54a3d --- /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,155 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } 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, catchError, defer, map, of, shareReplay, ReplaySubject, combineLatestWith } 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 { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; +import { Condition, ExecutionTargetTypeSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb'; +import { MatSelectModule } from '@angular/material/select'; +import { atLeastOneFieldValidator } from 'src/app/modules/form-field/validators/validators'; +import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module'; + +export type TargetInit = NonNullable< + NonNullable['targets']> +>[number]['type']; + +@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, + ], +}) +export class ActionsTwoAddActionTargetComponent { + protected readonly targetForm = this.buildActionTargetForm(); + + @Output() public readonly back = new EventEmitter(); + @Output() public readonly continue = new EventEmitter[]>(); + @Input() public hideBackButton = false; + @Input() set selectedCondition(selectedCondition: Condition | undefined) { + this.selectedCondition$.next(selectedCondition); + } + + private readonly selectedCondition$ = new ReplaySubject(1); + + protected readonly executionTargets$: Observable; + protected readonly executionConditions$: Observable; + + constructor( + private readonly fb: FormBuilder, + private readonly actionService: ActionService, + private readonly toast: ToastService, + ) { + this.executionTargets$ = this.listExecutionTargets().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.executionConditions$ = this.listExecutionConditions().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + } + + private buildActionTargetForm() { + return this.fb.group( + { + target: new FormControl(null, { validators: [] }), + executionConditions: new FormControl([], { validators: [] }), + }, + { + validators: atLeastOneFieldValidator(['target', 'executionConditions']), + }, + ); + } + + private listExecutionTargets() { + return defer(() => this.actionService.listTargets({})).pipe( + map(({ result }) => result.filter(this.targetHasDetailsAndConfig)), + catchError((error) => { + this.toast.showError(error); + return of([]); + }), + ); + } + + private listExecutionConditions(): Observable { + const selectedConditionJson$ = this.selectedCondition$.pipe(map((c) => JSON.stringify(c))); + + return defer(() => this.actionService.listExecutions({})).pipe( + combineLatestWith(selectedConditionJson$), + map(([executions, selectedConditionJson]) => + executions.result.map((e) => e?.condition).filter(this.conditionIsDefinedAndNotCurrentOne(selectedConditionJson)), + ), + + catchError((error) => { + this.toast.showError(error); + return of([]); + }), + ); + } + + private conditionIsDefinedAndNotCurrentOne(selectedConditionJson?: string) { + return (condition?: Condition): condition is Condition => { + if (!condition) { + // condition is undefined so it is not of type Condition + return false; + } + if (!selectedConditionJson) { + // condition is defined, and we don't have a selectedCondition so we can return all conditions + return true; + } + // we only return conditions that are not the same as the selectedCondition + return JSON.stringify(condition) !== selectedConditionJson; + }; + } + + private targetHasDetailsAndConfig(target: Target): target is Target { + return !!target.id && !!target.id; + } + + protected submit() { + const { target, executionConditions } = this.targetForm.getRawValue(); + + let valueToEmit: MessageInitShape[] = target + ? [ + { + type: { + case: 'target', + value: target.id, + }, + }, + ] + : []; + + const includeConditions: MessageInitShape[] = executionConditions + ? executionConditions.map((condition) => ({ + type: { + case: 'include', + value: condition, + }, + })) + : []; + + valueToEmit = [...valueToEmit, ...includeConditions]; + + this.continue.emit(valueToEmit); + } +} 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..496be167df --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.html @@ -0,0 +1,65 @@ +

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

+ +

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

+ +
+ + {{ 'ACTIONSTWO.TARGET.CREATE.NAME' | translate }} + + {{ + 'ACTIONSTWO.TARGET.CREATE.NAME_DESCRIPTION' | translate + }} + + + + {{ 'ACTIONSTWO.TARGET.CREATE.ENDPOINT' | translate }} + + {{ + 'ACTIONSTWO.TARGET.CREATE.ENDPOINT_DESCRIPTION' | translate + }} + + + + {{ 'ACTIONSTWO.TARGET.CREATE.TYPE' | translate }} + + + {{ 'ACTIONSTWO.TARGET.CREATE.TYPES.' + type | translate }} + + + + + + {{ 'ACTIONSTWO.TARGET.CREATE.TIMEOUT' | translate }} + + {{ + 'ACTIONSTWO.TARGET.CREATE.TIMEOUT_DESCRIPTION' | translate + }} + + + +
+ {{ 'ACTIONSTWO.TARGET.CREATE.INTERRUPT_ON_ERROR' | translate }} + {{ + 'ACTIONSTWO.TARGET.CREATE.INTERRUPT_ON_ERROR_DESCRIPTION' | translate + }} + {{ 'ACTIONSTWO.TARGET.CREATE.INTERRUPT_ON_ERROR_WARNING' | translate }} + +
+
+
+
+
+ + + + +
diff --git a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss new file mode 100644 index 0000000000..34a7d5203d --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss @@ -0,0 +1,25 @@ +.target-checkbox { + margin-bottom: 1rem; + + .target-condition-text { + display: flex; + flex-direction: column; + + .description { + font-size: 13px; + } + } +} + +.target-description { + margin-bottom: 0; +} + +.actions { + display: flex; + justify-content: space-between; +} + +.name-hint { + font-size: 12px; +} diff --git a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.ts b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.ts new file mode 100644 index 0000000000..b9c9c64853 --- /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) private 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..17a73304b0 --- /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..28d15b2a68 --- /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,33 @@ +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 { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb'; + +@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 delete = new EventEmitter(); + + @Input({ required: true }) + public set targets(targets: Target[] | null) { + this.targets$.next(targets); + } + + @Output() + public readonly selected = new EventEmitter(); + + private readonly targets$ = new ReplaySubject(1); + protected readonly dataSource$ = this.targets$.pipe( + filter(Boolean), + map((keys) => new MatTableDataSource(keys)), + ); +} diff --git a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html new file mode 100644 index 0000000000..a6bde66e41 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html @@ -0,0 +1,13 @@ +

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

+

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

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

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

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

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

- + diff --git a/console/src/app/modules/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..d82c3e3152 --- /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..38922cd1c7 --- /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..79b92e2214 100644 --- a/console/src/app/modules/settings-list/settings.ts +++ b/console/src/app/modules/settings-list/settings.ts @@ -35,6 +35,14 @@ export const OIDC: SidenavSetting = { }, }; +export const WEBKEYS: SidenavSetting = { + id: 'webkeys', + i18nKey: 'SETTINGS.LIST.WEB_KEYS', + requiredRoles: { + [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], + }, +}; + export const SECRETS: SidenavSetting = { id: 'secrets', i18nKey: 'SETTINGS.LIST.SECRETS', @@ -214,3 +222,23 @@ export const BRANDING: SidenavSetting = { [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], }, }; + +export const ACTIONS: SidenavSetting = { + id: 'actions', + i18nKey: 'SETTINGS.LIST.ACTIONS', + groupI18nKey: 'SETTINGS.GROUPS.ACTIONS', + requiredRoles: { + // todo: figure out roles + [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], + }, +}; + +export const ACTIONS_TARGETS: SidenavSetting = { + id: 'actions_targets', + i18nKey: 'SETTINGS.LIST.TARGETS', + groupI18nKey: 'SETTINGS.GROUPS.ACTIONS', + requiredRoles: { + // todo: figure out roles + [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], + }, +}; diff --git a/console/src/app/modules/sidenav/sidenav.component.html b/console/src/app/modules/sidenav/sidenav.component.html index d12a3e1c24..277686852b 100644 --- a/console/src/app/modules/sidenav/sidenav.component.html +++ b/console/src/app/modules/sidenav/sidenav.component.html @@ -1,12 +1,9 @@
-

{{ title }}

-

{{ description }}

- - - - {{ setting.groupI18nKey | translate }} + + {{ setting.groupI18nKey | translate }} - - - - - +
diff --git a/console/src/app/modules/sidenav/sidenav.component.ts b/console/src/app/modules/sidenav/sidenav.component.ts index e07c82dc51..33539750a2 100644 --- a/console/src/app/modules/sidenav/sidenav.component.ts +++ b/console/src/app/modules/sidenav/sidenav.component.ts @@ -1,7 +1,5 @@ -import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ChangeDetectionStrategy, Component, effect, EventEmitter, Input, Output, signal } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; - import { PolicyComponentServiceType } from '../policies/policy-component-types.enum'; export interface SidenavSetting { @@ -19,61 +17,61 @@ export interface SidenavSetting { selector: 'cnsl-sidenav', templateUrl: './sidenav.component.html', styleUrls: ['./sidenav.component.scss'], - providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SidenavComponent), multi: true }], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SidenavComponent implements ControlValueAccessor { - @Input() public title: string = ''; - @Input() public description: string = ''; +export class SidenavComponent { + @Input() public navigate: boolean = true; @Input() public indented: boolean = false; - @Input() public currentSetting?: string | undefined = undefined; - @Input() public settingsList: SidenavSetting[] = []; - @Input() public queryParam: string = ''; + @Input({ required: true }) public settingsList: SidenavSetting[] = []; + @Input({ required: true }) + public set setting(setting: SidenavSetting | null) { + if (!setting) { + return; + } + this.setting$.set(setting); + } + + @Output() + public settingChange = new EventEmitter(); + + protected readonly setting$ = signal(null); + + protected PolicyComponentServiceType = PolicyComponentServiceType; - public PolicyComponentServiceType: any = PolicyComponentServiceType; constructor( - private router: Router, - private route: ActivatedRoute, - ) {} + private readonly router: Router, + private readonly route: ActivatedRoute, + ) { + effect( + () => { + const setting = this.setting$(); + if (setting === null) { + return; + } - private onChange = (current: string | undefined) => {}; - private onTouch = (current: string | undefined) => {}; + this.settingChange.emit(setting); - @Input() get value(): string | undefined { - return this.currentSetting; + if (!this.navigate) { + return; + } + + this.router + .navigate([], { + relativeTo: this.route, + queryParams: { + id: setting ? setting.id : undefined, + }, + replaceUrl: true, + queryParamsHandling: 'merge', + skipLocationChange: false, + }) + .then(); + }, + { allowSignalWrites: true }, + ); } - set value(setting: string | undefined) { - this.currentSetting = setting; - - if (setting || setting === undefined || setting === '') { - this.onChange(setting); - this.onTouch(setting); - } - - if (this.queryParam && setting) { - this.router - .navigate([], { - relativeTo: this.route, - queryParams: { - [this.queryParam]: setting, - }, - replaceUrl: true, - queryParamsHandling: 'merge', - skipLocationChange: false, - }) - .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 @@
-
+
- - @@ -80,8 +75,8 @@
- - + +
- +
@@ -355,7 +350,7 @@ - +
@@ -435,7 +430,7 @@ - +

{{ 'APP.ADDITIONALORIGINSDESC' | translate }}

- +
- + = new Subject(); - public copiedKey: any = ''; - public InfoSectionType: any = InfoSectionType; + public InfoSectionType = InfoSectionType; public copied: string = ''; public settingsList: SidenavSetting[] = [{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' }]; - public currentSetting: string | undefined = this.settingsList[0].id; + public currentSetting = this.settingsList[0]; public isNew = signal(false); @@ -305,7 +303,7 @@ export class AppDetailComponent implements OnInit, OnDestroy { if (projectId && appId) { this.projectId = projectId; this.appId = appId; - this.getData(projectId, appId); + this.getData(projectId, appId).then(); } } @@ -395,7 +393,7 @@ export class AppDetailComponent implements OnInit, OnDestroy { if (this.initialAuthMethod === 'BASIC') { this.settingsList = [{ id: 'urls', i18nKey: 'APP.URLS' }]; - this.currentSetting = 'urls'; + this.currentSetting = this.settingsList[0]; } else { this.settingsList = [ { id: 'configuration', i18nKey: 'APP.CONFIGURATION' }, @@ -742,13 +740,13 @@ export class AppDetailComponent implements OnInit, OnDestroy { if (this.currentAuthMethod === 'BASIC') { this.settingsList = [{ id: 'urls', i18nKey: 'APP.URLS' }]; - this.currentSetting = 'urls'; + this.currentSetting = this.settingsList[0]; } else { this.settingsList = [ { id: 'configuration', i18nKey: 'APP.CONFIGURATION' }, { id: 'urls', i18nKey: 'APP.URLS' }, ]; - this.currentSetting = 'configuration'; + this.currentSetting = this.settingsList[0]; } } this.toast.showInfo('APP.TOAST.APIUPDATED', true); diff --git a/console/src/app/pages/projects/owned-projects/owned-project-detail/applications/applications.component.html b/console/src/app/pages/projects/owned-projects/owned-project-detail/applications/applications.component.html index e8d93a6654..fc425c8eca 100644 --- a/console/src/app/pages/projects/owned-projects/owned-project-detail/applications/applications.component.html +++ b/console/src/app/pages/projects/owned-projects/owned-project-detail/applications/applications.component.html @@ -2,7 +2,6 @@ [loading]="dataSource.loading$ | async" [selection]="selection" (refreshed)="refreshPage()" - [dataSize]="dataSource.totalResult" [timestamp]="dataSource.viewTimestamp" > diff --git a/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail.component.html b/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail.component.html index a6941d62c4..5315dd15ac 100644 --- a/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail.component.html +++ b/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail.component.html @@ -66,8 +66,8 @@
- - + + - + - +

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

{{ 'PROJECT.ROLE.DESCRIPTION' | translate }}

@@ -172,12 +172,12 @@ >
- +

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

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

- +

{{ 'GRANTS.TITLE' | translate }}

{{ 'GRANTS.DESC' | translate }}

diff --git a/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail.component.ts b/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail.component.ts index 021b0dc2b0..9a17eefb7b 100644 --- a/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail.component.ts +++ b/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail.component.ts @@ -1,7 +1,6 @@ import { Location } from '@angular/common'; import { Component, EventEmitter, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { MatTableDataSource } from '@angular/material/table'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, from, Observable, of } from 'rxjs'; @@ -12,8 +11,7 @@ import { ProjectPrivateLabelingDialogComponent } from 'src/app/modules/project-p import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component'; import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; -import { App } from 'src/app/proto/generated/zitadel/app_pb'; -import { ListAppsResponse, UpdateProjectRequest } from 'src/app/proto/generated/zitadel/management_pb'; +import { UpdateProjectRequest } from 'src/app/proto/generated/zitadel/management_pb'; import { Member } from 'src/app/proto/generated/zitadel/member_pb'; import { PrivateLabelingSetting, Project, ProjectState } from 'src/app/proto/generated/zitadel/project_pb'; import { User } from 'src/app/proto/generated/zitadel/user_pb'; @@ -21,7 +19,7 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/ import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; -import { NameDialogComponent } from '../../../../modules/name-dialog/name-dialog.component'; +import { NameDialogComponent } from 'src/app/modules/name-dialog/name-dialog.component'; const ROUTEPARAM = 'projectid'; @@ -39,11 +37,6 @@ export class OwnedProjectDetailComponent implements OnInit { public projectId: string = ''; public project?: Project.AsObject; - public pageSizeApps: number = 10; - public appsDataSource: MatTableDataSource = new MatTableDataSource(); - public appsResult!: ListAppsResponse.AsObject; - public appsColumns: string[] = ['name']; - public ProjectState: any = ProjectState; public ChangeType: any = ChangeType; @@ -61,22 +54,25 @@ export class OwnedProjectDetailComponent implements OnInit { public refreshChanges$: EventEmitter = new EventEmitter(); public settingsList: SidenavSetting[] = [GENERAL, ROLES, PROJECTGRANTS, GRANTS]; - public currentSetting: string = ''; + public currentSetting = this.settingsList[0]; + constructor( public translate: TranslateService, private route: ActivatedRoute, private toast: ToastService, private mgmtService: ManagementService, - private _location: Location, private dialog: MatDialog, private router: Router, private breadcrumbService: BreadcrumbService, ) { - this.currentSetting = 'general'; - route.queryParams.pipe(take(1)).subscribe((params: Params) => { - const { id } = params; - if (id) { - this.currentSetting = id; + route.queryParamMap.pipe(take(1)).subscribe((params) => { + const id = params.get('id'); + if (!id) { + return; + } + const setting = this.settingsList.find((setting) => setting.id === id); + if (setting) { + this.currentSetting = setting; } }); } @@ -85,7 +81,7 @@ export class OwnedProjectDetailComponent implements OnInit { const projectId = this.route.snapshot.paramMap.get(ROUTEPARAM); if (projectId) { this.projectId = projectId; - this.getData(projectId); + this.getData(projectId).then(); } } @@ -249,7 +245,7 @@ export class OwnedProjectDetailComponent implements OnInit { const params: Params = { deferredReload: true, }; - this.router.navigate(['/projects'], { queryParams: params }); + this.router.navigate(['/projects'], { queryParams: params }).then(); }) .catch((error) => { this.toast.showError(error); @@ -280,10 +276,6 @@ export class OwnedProjectDetailComponent implements OnInit { } } - public navigateBack(): void { - this._location.back(); - } - public updateName(): void { this.saveProject(); } @@ -323,7 +315,7 @@ export class OwnedProjectDetailComponent implements OnInit { public showDetail(): void { if (this.project) { - this.router.navigate(['projects', this.project.id, 'members']); + this.router.navigate(['projects', this.project.id, 'members']).then(); } } } diff --git a/console/src/app/pages/projects/owned-projects/project-grants/project-grants.component.html b/console/src/app/pages/projects/owned-projects/project-grants/project-grants.component.html index 07d5baaa8c..1e62ced5c5 100644 --- a/console/src/app/pages/projects/owned-projects/project-grants/project-grants.component.html +++ b/console/src/app/pages/projects/owned-projects/project-grants/project-grants.component.html @@ -5,7 +5,6 @@ [loading]="dataSource.loading$ | async" *ngIf="projectId" (refreshed)="refreshPage()" - [dataSize]="dataSource.totalResult" [selection]="selection" [timestamp]="dataSource.viewTimestamp" (refreshed)="getRoleOptions(projectId)" diff --git a/console/src/app/pages/projects/project-list/project-list.component.html b/console/src/app/pages/projects/project-list/project-list.component.html index d51d9f9ba2..dadf5056fe 100644 --- a/console/src/app/pages/projects/project-list/project-list.component.html +++ b/console/src/app/pages/projects/project-list/project-list.component.html @@ -2,7 +2,6 @@
+ + {{ 'USER.PROFILE.USERNAME' | translate }} + + + {{ 'USER.PROFILE.FIRSTNAME' | translate }} @@ -29,14 +34,6 @@ {{ 'USER.PROFILE.LASTNAME' | translate }} - - {{ 'USER.PROFILE.NICKNAME' | translate }} - - - - {{ 'USER.PROFILE.USERNAME' | translate }} - -
(authenticationFactor, { nonNullable: true, @@ -188,7 +187,6 @@ export class UserCreateV2Component implements OnInit { profile: { givenName: userValues.givenName, familyName: userValues.familyName, - nickName: userValues.nickName, }, email: { email: userValues.email, 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/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/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/grpc.service.ts b/console/src/app/services/grpc.service.ts index 81e7893671..b2f89ca648 100644 --- a/console/src/app/services/grpc.service.ts +++ b/console/src/app/services/grpc.service.ts @@ -18,10 +18,22 @@ import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } f import { StorageService } from './storage.service'; import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb'; //@ts-ignore -import { createFeatureServiceClient, 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'; +// @ts-ignore +import { createClientFor } from '@zitadel/client'; +import { Client, Transport } from '@connectrpc/connect'; + +import { WebKeyService } from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb'; +import { ActionService } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; + +// @ts-ignore +import { createClientFor } from '@zitadel/client'; + +const createWebKeyServiceClient = createClientFor(WebKeyService); +const createActionServiceClient = createClientFor(ActionService); @Injectable({ providedIn: 'root', @@ -32,15 +44,17 @@ export class GrpcService { public admin!: AdminServiceClient; 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, @@ -105,9 +119,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/new-feature.service.ts b/console/src/app/services/new-feature.service.ts index 6cc631755e..5bb4fe8dd7 100644 --- a/console/src/app/services/new-feature.service.ts +++ b/console/src/app/services/new-feature.service.ts @@ -3,7 +3,6 @@ import { GrpcService } from './grpc.service'; import { GetInstanceFeaturesResponse, ResetInstanceFeaturesResponse, - SetInstanceFeaturesRequest, SetInstanceFeaturesRequestSchema, SetInstanceFeaturesResponse, } from '@zitadel/proto/zitadel/feature/v2/instance_pb'; 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/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/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index c37a88917b..f1929cef14 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -185,6 +185,32 @@ "DESCRIPTION": "Животът на неактивния refresh токен е максималното време, през което refresh токен може да не се използва." } }, + "WEB_KEYS": { + "DESCRIPTION": "Управлявайте вашите OIDC уеб ключове, за да подписвате и валидирате токени сигурно за вашата ZITADEL инстанция.", + "TABLE": { + "TITLE": "Активни и бъдещи уеб ключове", + "DESCRIPTION": "Вашите активни и предстоящи уеб ключове. Активирането на нов ключ ще деактивира текущия.", + "NOTE": "Забележка: Крайна точка JWKs OIDC връща кешируем отговор (по подразбиране 5 минути). Избягвайте активирането на ключ твърде рано, тъй като той може да не е наличен в кеша и клиентите.", + "ACTIVATE": "Активирайте следващия уеб ключ", + "ACTIVE": "В момента активен", + "NEXT": "Следващ в опашката", + "FUTURE": "Бъдещ", + "WARNING": "Уеб ключът е на по-малко от 5 минути" + }, + "CREATE": { + "TITLE": "Създаване на нов уеб ключ", + "DESCRIPTION": "Създаването на нов уеб ключ го добавя към вашия списък. ZITADEL използва ключове RSA2048 с хеш SHA256 по подразбиране.", + "KEY_TYPE": "Тип ключ", + "BITS": "Битове", + "HASHER": "Хешер", + "CURVE": "Крива" + }, + "PREVIOUS_TABLE": { + "TITLE": "Предишни уеб ключове", + "DESCRIPTION": "Това са вашите предишни уеб ключове, които вече не са активни.", + "DEACTIVATED_ON": "Деактивиран на" + } + }, "MESSAGE_TEXTS": { "TITLE": "Текстове на съобщенията", "DESCRIPTION": "Персонализирайте текстовете на вашите имейл или SMS уведомления. Ако искате да деактивирате някои от езиците, ограничете ги в настройките за език на вашите инстанции.", @@ -501,6 +527,114 @@ "DOWNLOAD": "Изтегляне", "APPLY": "Прилагам" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Действия", + "DESCRIPTION": "Действията ви позволяват да изпълнявате персонализиран код в отговор на API заявки, събития или специфични функции. Използвайте ги, за да разширите Zitadel, да автоматизирате работни процеси и да се интегрирате с други системи.", + "TYPES": { + "request": "Заявка", + "response": "Отговор", + "events": "Събития", + "function": "Функция" + }, + "DIALOG": { + "CREATE_TITLE": "Създаване на действие", + "UPDATE_TITLE": "Актуализиране на действие", + "TYPE": { + "DESCRIPTION": "Изберете кога искате да се изпълни това действие", + "REQUEST": { + "TITLE": "Заявка", + "DESCRIPTION": "Заявки, които се появяват в Zitadel. Това може да бъде нещо като заявка за вход." + }, + "RESPONSE": { + "TITLE": "Отговор", + "DESCRIPTION": "Отговор от заявка в Zitadel. Помислете за отговора, който получавате при извличане на потребител." + }, + "EVENTS": { + "TITLE": "Събития", + "DESCRIPTION": "Събития, които се случват в Zitadel. Това може да бъде нещо като създаване на потребителски акаунт, успешно влизане и т.н." + }, + "FUNCTIONS": { + "TITLE": "Функции", + "DESCRIPTION": "Функции, които можете да извикате в Zitadel. Това може да бъде всичко от изпращане на имейл до създаване на потребител." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Изберете дали това действие се прилага към всички заявки, конкретна услуга (напр. управление на потребители) или единична заявка (напр. създаване на потребител).", + "ALL": { + "TITLE": "Всички", + "DESCRIPTION": "Изберете това, ако искате да изпълните действието си при всяка заявка" + }, + "SELECT_SERVICE": { + "TITLE": "Избор на услуга", + "DESCRIPTION": "Изберете услуга на Zitadel за вашето действие." + }, + "SELECT_METHOD": { + "TITLE": "Избор на метод", + "DESCRIPTION": "Ако искате да изпълните само при конкретна заявка, изберете я тук", + "NOTE": "Ако не изберете метод, действието ви ще се изпълни при всяка заявка във вашата избрана услуга." + }, + "FUNCTIONNAME": { + "TITLE": "Име на функция", + "DESCRIPTION": "Изберете функцията, която искате да изпълните" + }, + "SELECT_GROUP": { + "TITLE": "Задаване на група", + "DESCRIPTION": "Ако искате да изпълните само върху група събития, задайте групата тук" + }, + "SELECT_EVENT": { + "TITLE": "Избор на събитие", + "DESCRIPTION": "Ако искате да изпълните само при конкретно събитие, посочете го тук" + } + }, + "TARGET": { + "DESCRIPTION": "Можете да изберете да изпълните цел или да я изпълните при същите условия като други цели.", + "TARGET": { + "DESCRIPTION": "Целта, която искате да изпълните за това действие" + }, + "CONDITIONS": { + "DESCRIPTION": "Условия за изпълнение" + } + } + }, + "TABLE": { + "CONDITION": "Условие", + "TYPE": "Тип", + "TARGET": "Цел", + "CREATIONDATE": "Дата на създаване" + } + }, + "TARGET": { + "TITLE": "Цели", + "DESCRIPTION": "Целта е дестинацията на кода, който искате да изпълните от действие. Създайте цел тук и я добавете към вашите действия.", + "CREATE": { + "TITLE": "Създаване на вашата цел", + "DESCRIPTION": "Създайте своя собствена цел извън Zitadel", + "NAME": "Име", + "NAME_DESCRIPTION": "Дайте на целта си ясно, описателно име, за да я идентифицирате лесно по-късно", + "TYPE": "Тип", + "TYPES": { + "restWebhook": "REST уеб кука", + "restCall": "REST извикване", + "restAsync": "REST асинхронно" + }, + "ENDPOINT": "Крайна точка", + "ENDPOINT_DESCRIPTION": "Въведете крайната точка, където се хоства вашият код. Уверете се, че е достъпна за нас!", + "TIMEOUT": "Време за изчакване", + "TIMEOUT_DESCRIPTION": "Задайте максималното време, за което вашата цел трябва да отговори. Ако отнеме повече време, ще спрем заявката.", + "INTERRUPT_ON_ERROR": "Прекъсване при грешка", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Спрете всички изпълнения, когато целите върнат грешка", + "INTERRUPT_ON_ERROR_WARNING": "Внимание: „Прекъсване при грешка“ спира операциите при неуспех, което може да доведе до блокиране. Тествайте с изключена опция, за да предотвратите блокиране на входа/създаването.", + "AWAIT_RESPONSE": "Изчакване на отговор", + "AWAIT_RESPONSE_DESCRIPTION": "Ще изчакаме отговор, преди да направим нещо друго. Полезно, ако възнамерявате да използвате множество цели за едно действие" + }, + "TABLE": { + "NAME": "Име", + "ENDPOINT": "Крайна точка", + "CREATIONDATE": "Дата на създаване" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Има контрол върху цялата инстанция, включително всички организации", "IAM_OWNER_VIEWER": "Има разрешение да прегледа целия екземпляр, включително всички организации", @@ -1355,6 +1489,7 @@ "BRANDING": "Брандиране", "PRIVACYPOLICY": "Политика за бедност", "OIDC": "Живот и изтичане на OIDC Token", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Тайна поява", "SECURITY": "Настройки на сигурността", "EVENTS": "Събития", @@ -1514,7 +1649,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": { diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index 914fb290bb..ba7f4b1ada 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -185,6 +185,32 @@ "DESCRIPTION": "Životnost nečinného refresh tokenu je maximální doba, po kterou může být refresh token nepoužitý." } }, + "WEB_KEYS": { + "DESCRIPTION": "Spravujte své OIDC webové klíče pro bezpečné podepisování a ověřování tokenů ve vaší instanci ZITADEL.", + "TABLE": { + "TITLE": "Aktivní a budoucí webové klíče", + "DESCRIPTION": "Vaše aktivní a nadcházející webové klíče. Aktivací nového klíče dojde k deaktivaci aktuálního.", + "NOTE": "Poznámka: Koncový bod JWKs OIDC vrací odpověď uložitelnou do mezipaměti (výchozí 5 minut). Vyhněte se příliš brzké aktivaci klíče, protože nemusí být dostupný v mezipaměti a klientům.", + "ACTIVATE": "Aktivovat další webový klíč", + "ACTIVE": "Aktuálně aktivní", + "NEXT": "Další v řadě", + "FUTURE": "Budoucí", + "WARNING": "Webový klíč je starý méně než 5 minut" + }, + "CREATE": { + "TITLE": "Vytvořit nový webový klíč", + "DESCRIPTION": "Vytvořením nového webového klíče jej přidáte do svého seznamu. ZITADEL používá klíče RSA2048 s hashováním SHA256 jako výchozí.", + "KEY_TYPE": "Typ klíče", + "BITS": "Bity", + "HASHER": "Hasher", + "CURVE": "Křivka" + }, + "PREVIOUS_TABLE": { + "TITLE": "Předchozí webové klíče", + "DESCRIPTION": "Toto jsou vaše předchozí webové klíče, které již nejsou aktivní.", + "DEACTIVATED_ON": "Deaktivováno dne" + } + }, "MESSAGE_TEXTS": { "TITLE": "Texty zpráv", "DESCRIPTION": "Přizpůsob si texty svých e-mailových nebo SMS notifikací. Pokud chceš některé z jazyků zakázat, omez je ve svém nastavení jazyků instance.", @@ -502,6 +528,114 @@ "DOWNLOAD": "Stáhnout", "APPLY": "Platit" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Akce", + "DESCRIPTION": "Akce vám umožňují spouštět vlastní kód v reakci na požadavky API, události nebo specifické funkce. Použijte je k rozšíření Zitadel, automatizaci pracovních postupů a integraci s dalšími systémy.", + "TYPES": { + "request": "Požadavek", + "response": "Odpověď", + "events": "Události", + "function": "Funkce" + }, + "DIALOG": { + "CREATE_TITLE": "Vytvořit akci", + "UPDATE_TITLE": "Aktualizovat akci", + "TYPE": { + "DESCRIPTION": "Vyberte, kdy chcete tuto akci spustit", + "REQUEST": { + "TITLE": "Požadavek", + "DESCRIPTION": "Požadavky, které se vyskytují v rámci Zitadel. Může to být něco jako volání požadavku na přihlášení." + }, + "RESPONSE": { + "TITLE": "Odpověď", + "DESCRIPTION": "Odpověď na požadavek v rámci Zitadel. Představte si odpověď, kterou získáte při načítání uživatele." + }, + "EVENTS": { + "TITLE": "Události", + "DESCRIPTION": "Události, které se dějí v rámci Zitadel. Může to být cokoli, jako je vytvoření uživatelského účtu, úspěšné přihlášení atd." + }, + "FUNCTIONS": { + "TITLE": "Funkce", + "DESCRIPTION": "Funkce, které můžete volat v rámci Zitadel. Může to být cokoli od odeslání e-mailu po vytvoření uživatele." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Vyberte, zda se tato akce vztahuje na všechny požadavky, konkrétní službu (např. správa uživatelů) nebo jeden požadavek (např. vytvoření uživatele).", + "ALL": { + "TITLE": "Všechny", + "DESCRIPTION": "Vyberte tuto možnost, pokud chcete spustit akci pro každý požadavek" + }, + "SELECT_SERVICE": { + "TITLE": "Vybrat službu", + "DESCRIPTION": "Vyberte službu Zitadel pro svou akci." + }, + "SELECT_METHOD": { + "TITLE": "Vybrat metodu", + "DESCRIPTION": "Pokud chcete spustit pouze pro konkrétní požadavek, vyberte jej zde", + "NOTE": "Pokud nevyberete metodu, vaše akce se spustí pro každý požadavek ve vaší vybrané službě." + }, + "FUNCTIONNAME": { + "TITLE": "Název funkce", + "DESCRIPTION": "Vyberte funkci, kterou chcete spustit" + }, + "SELECT_GROUP": { + "TITLE": "Nastavit skupinu", + "DESCRIPTION": "Pokud chcete spustit pouze pro skupinu událostí, nastavte zde skupinu" + }, + "SELECT_EVENT": { + "TITLE": "Vybrat událost", + "DESCRIPTION": "Pokud chcete spustit pouze pro konkrétní událost, zadejte ji zde" + } + }, + "TARGET": { + "DESCRIPTION": "Můžete se rozhodnout spustit cíl nebo jej spustit za stejných podmínek jako jiné cíle.", + "TARGET": { + "DESCRIPTION": "Cíl, který chcete spustit pro tuto akci" + }, + "CONDITIONS": { + "DESCRIPTION": "Podmínky spuštění" + } + } + }, + "TABLE": { + "CONDITION": "Podmínka", + "TYPE": "Typ", + "TARGET": "Cíl", + "CREATIONDATE": "Datum vytvoření" + } + }, + "TARGET": { + "TITLE": "Cíle", + "DESCRIPTION": "Cíl je cíl kódu, který chcete spustit z akce. Vytvořte zde cíl a přidejte jej do svých akcí.", + "CREATE": { + "TITLE": "Vytvořit cíl", + "DESCRIPTION": "Vytvořte si vlastní cíl mimo Zitadel", + "NAME": "Název", + "NAME_DESCRIPTION": "Dejte svému cíli jasný, popisný název, aby bylo snadné jej později identifikovat", + "TYPE": "Typ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Volání", + "restAsync": "REST Asynchronní" + }, + "ENDPOINT": "Koncový bod", + "ENDPOINT_DESCRIPTION": "Zadejte koncový bod, kde je hostován váš kód. Ujistěte se, že je pro nás přístupný!", + "TIMEOUT": "Časový limit", + "TIMEOUT_DESCRIPTION": "Nastavte maximální dobu, po kterou musí váš cíl odpovědět. Pokud to trvá déle, požadavek zastavíme.", + "INTERRUPT_ON_ERROR": "Přerušit při chybě", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Zastavte všechna spuštění, když cíle vrátí chybu", + "INTERRUPT_ON_ERROR_WARNING": "Pozor: „Přerušit při chybě“ zastaví operace při selhání, což může vést k zablokování. Otestujte s vypnutou možností, abyste předešli zablokování přihlášení/vytváření.", + "AWAIT_RESPONSE": "Čekat na odpověď", + "AWAIT_RESPONSE_DESCRIPTION": "Před provedením čehokoli jiného počkáme na odpověď. Užitečné, pokud hodláte použít více cílů pro jednu akci" + }, + "TABLE": { + "NAME": "Název", + "ENDPOINT": "Koncový bod", + "CREATIONDATE": "Datum vytvoření" + } + } + }, "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í", @@ -1356,6 +1490,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", @@ -1515,7 +1650,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": { diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 5735d383d6..79b9596678 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -185,6 +185,32 @@ "DESCRIPTION": "Die maximale Inaktivitätsdauer eines Aktualisierungstokens ist die maximale Zeit, in der ein Aktualisierungstoken unbenutzt sein kann." } }, + "WEB_KEYS": { + "DESCRIPTION": "Verwalte deine OIDC Web Keys, um Tokens für deine ZITADEL-Instanz sicher zu signieren und zu validieren.", + "TABLE": { + "TITLE": "Aktive und zukünftige Web Keys", + "DESCRIPTION": "Deine aktiven und kommenden Web Keys. Das Aktivieren eines neuen Schlüssels deaktiviert den aktuellen.", + "NOTE": "Hinweis: Der JWKs OIDC-Endpunkt gibt eine zwischenspeicherbare Antwort zurück (Standard: 5 Min.). Vermeide es, einen Schlüssel zu früh zu aktivieren, da er möglicherweise noch nicht in Caches und Clients verfügbar ist.", + "ACTIVATE": "Nächsten Web Key aktivieren", + "ACTIVE": "Derzeit aktiv", + "NEXT": "Als Nächstes in der Warteschlange", + "FUTURE": "Zukünftig", + "WARNING": "Der Web Key ist weniger als 5 Minuten alt" + }, + "CREATE": { + "TITLE": "Neuen Web Key erstellen", + "DESCRIPTION": "Das Erstellen eines neuen Web Keys fügt ihn zu deiner Liste hinzu. ZITADEL verwendet standardmäßig RSA2048-Schlüssel mit einem SHA256-Hasher.", + "KEY_TYPE": "Schlüsseltyp", + "BITS": "Bits", + "HASHER": "Hasher", + "CURVE": "Kurve" + }, + "PREVIOUS_TABLE": { + "TITLE": "Frühere Web Keys", + "DESCRIPTION": "Dies sind deine früheren Web Keys, die nicht mehr aktiv sind.", + "DEACTIVATED_ON": "Deaktiviert am" + } + }, "MESSAGE_TEXTS": { "TITLE": "Nachrichtentexte", "DESCRIPTION": "Passe die Texte deiner Benachrichtigungs-E-Mails oder SMS-Nachrichten an. Wenn du einige der Sprachen deaktivieren möchtest, beschränke sie in den Spracheinstellungen deiner Instanz.", @@ -502,6 +528,114 @@ "DOWNLOAD": "Herunterladen", "APPLY": "Anwenden" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Aktionen", + "DESCRIPTION": "Aktionen ermöglichen es Ihnen, benutzerdefinierten Code als Reaktion auf API-Anfragen, Ereignisse oder bestimmte Funktionen auszuführen. Verwenden Sie sie, um Zitadel zu erweitern, Arbeitsabläufe zu automatisieren und sich in andere Systeme zu integrieren.", + "TYPES": { + "request": "Anfrage", + "response": "Antwort", + "events": "Ereignisse", + "function": "Funktion" + }, + "DIALOG": { + "CREATE_TITLE": "Eine Aktion erstellen", + "UPDATE_TITLE": "Eine Aktion aktualisieren", + "TYPE": { + "DESCRIPTION": "Wählen Sie aus, wann diese Aktion ausgeführt werden soll", + "REQUEST": { + "TITLE": "Anfrage", + "DESCRIPTION": "Anfragen, die innerhalb von Zitadel auftreten. Dies könnte so etwas wie ein Login-Anfrageaufruf sein." + }, + "RESPONSE": { + "TITLE": "Antwort", + "DESCRIPTION": "Eine Antwort auf eine Anfrage innerhalb von Zitadel. Denken Sie an die Antwort, die Sie beim Abrufen eines Benutzers erhalten." + }, + "EVENTS": { + "TITLE": "Ereignisse", + "DESCRIPTION": "Ereignisse, die innerhalb von Zitadel stattfinden. Dies könnte alles sein, wie z.B. das Erstellen eines Benutzerkontos, ein erfolgreicher Login usw." + }, + "FUNCTIONS": { + "TITLE": "Funktionen", + "DESCRIPTION": "Funktionen, die Sie innerhalb von Zitadel aufrufen können. Dies könnte alles sein, vom Senden einer E-Mail bis zum Erstellen eines Benutzers." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Wählen Sie aus, ob diese Aktion für alle Anfragen, einen bestimmten Dienst (z.B. Benutzerverwaltung) oder eine einzelne Anfrage (z.B. Benutzer erstellen) gelten soll.", + "ALL": { + "TITLE": "Alle", + "DESCRIPTION": "Wählen Sie dies aus, wenn Sie Ihre Aktion bei jeder Anfrage ausführen möchten" + }, + "SELECT_SERVICE": { + "TITLE": "Dienst auswählen", + "DESCRIPTION": "Wählen Sie einen Zitadel-Dienst für Ihre Aktion aus." + }, + "SELECT_METHOD": { + "TITLE": "Methode auswählen", + "DESCRIPTION": "Wenn Sie nur bei einer bestimmten Anfrage ausführen möchten, wählen Sie sie hier aus", + "NOTE": "Wenn Sie keine Methode auswählen, wird Ihre Aktion bei jeder Anfrage in Ihrem ausgewählten Dienst ausgeführt." + }, + "FUNCTIONNAME": { + "TITLE": "Funktionsname", + "DESCRIPTION": "Wählen Sie die Funktion aus, die Sie ausführen möchten" + }, + "SELECT_GROUP": { + "TITLE": "Gruppe festlegen", + "DESCRIPTION": "Wenn Sie nur bei einer Gruppe von Ereignissen ausführen möchten, legen Sie die Gruppe hier fest" + }, + "SELECT_EVENT": { + "TITLE": "Ereignis auswählen", + "DESCRIPTION": "Wenn Sie nur bei einem bestimmten Ereignis ausführen möchten, geben Sie es hier an" + } + }, + "TARGET": { + "DESCRIPTION": "Sie können wählen, ob Sie ein Ziel ausführen oder es unter den gleichen Bedingungen wie andere Ziele ausführen möchten.", + "TARGET": { + "DESCRIPTION": "Das Ziel, das Sie für diese Aktion ausführen möchten" + }, + "CONDITIONS": { + "DESCRIPTION": "Ausführungsbedingungen" + } + } + }, + "TABLE": { + "CONDITION": "Bedingung", + "TYPE": "Typ", + "TARGET": "Ziel", + "CREATIONDATE": "Erstellungsdatum" + } + }, + "TARGET": { + "TITLE": "Ziele", + "DESCRIPTION": "Ein Ziel ist das Ziel des Codes, den Sie von einer Aktion ausführen möchten. Erstellen Sie hier ein Ziel und fügen Sie es Ihren Aktionen hinzu.", + "CREATE": { + "TITLE": "Ihr Ziel erstellen", + "DESCRIPTION": "Erstellen Sie Ihr eigenes Ziel außerhalb von Zitadel", + "NAME": "Name", + "NAME_DESCRIPTION": "Geben Sie Ihrem Ziel einen klaren, beschreibenden Namen, um es später leicht identifizieren zu können", + "TYPE": "Typ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Aufruf", + "restAsync": "REST Asynchron" + }, + "ENDPOINT": "Endpunkt", + "ENDPOINT_DESCRIPTION": "Geben Sie den Endpunkt ein, an dem Ihr Code gehostet wird. Stellen Sie sicher, dass er für uns zugänglich ist!", + "TIMEOUT": "Timeout", + "TIMEOUT_DESCRIPTION": "Legen Sie die maximale Zeit fest, die Ihr Ziel zum Antworten hat. Wenn es länger dauert, stoppen wir die Anfrage.", + "INTERRUPT_ON_ERROR": "Bei Fehler unterbrechen", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Stoppen Sie alle Ausführungen, wenn die Ziele einen Fehler zurückgeben", + "INTERRUPT_ON_ERROR_WARNING": "Achtung: „Bei Fehler unterbrechen“ stoppt Vorgänge bei einem Fehler und kann zur Sperrung führen. Testen Sie mit deaktivierter Option, um Login/Erstellung nicht zu blockieren.", + "AWAIT_RESPONSE": "Auf Antwort warten", + "AWAIT_RESPONSE_DESCRIPTION": "Wir warten auf eine Antwort, bevor wir etwas anderes tun. Nützlich, wenn Sie mehrere Ziele für eine einzelne Aktion verwenden möchten" + }, + "TABLE": { + "NAME": "Name", + "ENDPOINT": "Endpunkt", + "CREATIONDATE": "Erstellungsdatum" + } + } + }, "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", @@ -1356,6 +1490,7 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Datenschutzrichtlinie", "OIDC": "OIDC Token Lifetime und Expiration", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Secret Generator", "SECURITY": "Sicherheitseinstellungen", "EVENTS": "Events", @@ -1515,7 +1650,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": { diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index e09b138708..19759d0041 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -185,6 +185,32 @@ "DESCRIPTION": "The idle refresh token lifetime is the maximum time a refresh token can be unused." } }, + "WEB_KEYS": { + "DESCRIPTION": "Manage your OIDC Web Keys to securely sign and validate tokens for your ZITADEL instance.", + "TABLE": { + "TITLE": "Active and Future Web Keys", + "DESCRIPTION": "Your active and upcoming web keys. Activating a new key will deactivate the current one.", + "NOTE": "Note: The JWKs OIDC endpoint returns a cacheable response (default 5 min). Avoid activating a key too soon, as it may not be available to caches and clients yet.", + "ACTIVATE": "Activate next Web Key", + "ACTIVE": "Currently active", + "NEXT": "Next in queue", + "FUTURE": "Future", + "WARNING": "Web Key is less than 5 min old" + }, + "CREATE": { + "TITLE": "Create new Web Key", + "DESCRIPTION": "Creating a new web key adds it to your list. ZITADEL uses RSA2048 keys with a SHA256 hasher by default.", + "KEY_TYPE": "Key Type", + "BITS": "Bits", + "HASHER": "Hasher", + "CURVE": "Curve" + }, + "PREVIOUS_TABLE": { + "TITLE": "Previous Web Keys", + "DESCRIPTION": "These are your previous web keys that are no longer active.", + "DEACTIVATED_ON": "Deactivated on" + } + }, "MESSAGE_TEXTS": { "TITLE": "Message Texts", "DESCRIPTION": "Customize the texts of your notification email or SMS messages. If you want to disable some of the languages, restrict them in your instances language settings.", @@ -502,6 +528,114 @@ "DOWNLOAD": "Download", "APPLY": "Apply" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Actions", + "DESCRIPTION": "Actions let you run custom code in response to API requests, events or specific functions. Use them to extend Zitadel, automate workflows, and itegrate with other systems.", + "TYPES": { + "request": "Request", + "response": "Response", + "events": "Events", + "function": "Function" + }, + "DIALOG": { + "CREATE_TITLE": "Create an Action", + "UPDATE_TITLE": "Update an Action", + "TYPE": { + "DESCRIPTION": "Select when you want this Action to run", + "REQUEST": { + "TITLE": "Request", + "DESCRIPTION": "Requests that occur within Zitadel. This could be something as a login request call." + }, + "RESPONSE": { + "TITLE": "Response", + "DESCRIPTION": "A response from a request within Zitadel. Think of the response you get back from fetching a user." + }, + "EVENTS": { + "TITLE": "Events", + "DESCRIPTION": "Events that happen within Zitadel. This could be anything like a user creating an account, a successful login etc." + }, + "FUNCTIONS": { + "TITLE": "Functions", + "DESCRIPTION": "Functions that you can call within Zitadel. This could be anything from sending an email to creating a user." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Choose whether this action applies to all request, a specific service (ec. user management), or a single request (e.g. create user).", + "ALL": { + "TITLE": "All", + "DESCRIPTION": "Select this if you want to run your action on every request" + }, + "SELECT_SERVICE": { + "TITLE": "Select Service", + "DESCRIPTION": "Choose a Zitadel Service for you action." + }, + "SELECT_METHOD": { + "TITLE": "Select Method", + "DESCRIPTION": "If you want to only execute on a specific request, select it here", + "NOTE": "If you don't select a method, your action will run on every request in your selected service." + }, + "FUNCTIONNAME": { + "TITLE": "Function Name", + "DESCRIPTION": "Choose the function you want to execute" + }, + "SELECT_GROUP": { + "TITLE": "Set Group", + "DESCRIPTION": "If you want to only execute on a group of events, set the group here" + }, + "SELECT_EVENT": { + "TITLE": "Select Event", + "DESCRIPTION": "If you want to only execute on a specific event, specify it here" + } + }, + "TARGET": { + "DESCRIPTION": "You can choose to execute a target, or to run it on the same conditions as other targets.", + "TARGET": { + "DESCRIPTION": "The target you want to execute for this action" + }, + "CONDITIONS": { + "DESCRIPTION": "Execution Conditions" + } + } + }, + "TABLE": { + "CONDITION": "Condition", + "TYPE": "Type", + "TARGET": "Target", + "CREATIONDATE": "Creation Date" + } + }, + "TARGET": { + "TITLE": "Targets", + "DESCRIPTION": "A target is the destination of the code you want to execute from an action. Create a target here and at it to your actions.", + "CREATE": { + "TITLE": "Create your Target", + "DESCRIPTION": "Create your own target outside of Zitadel", + "NAME": "Name", + "NAME_DESCRIPTION": "Give your target a clear, descriptive name to make it easy to identify later", + "TYPE": "Type", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Call", + "restAsync": "REST Async" + }, + "ENDPOINT": "Endpoint", + "ENDPOINT_DESCRIPTION": "Enter the endpoint where your code is hosted. Make sure it is accessible to us!", + "TIMEOUT": "Timeout", + "TIMEOUT_DESCRIPTION": "Set the maximum time your target has to respond. If it takes longer, we will stop the request.", + "INTERRUPT_ON_ERROR": "Interrupt on Error", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Stop all executions when the targets returns with an error", + "INTERRUPT_ON_ERROR_WARNING": "Caution: “Interrupt on Error” halts operations on failure, risking lockout. Test with it disabled to prevent blocking login/creation.", + "AWAIT_RESPONSE": "Await Response", + "AWAIT_RESPONSE_DESCRIPTION": "We'll Wait for a response before we do anything else. Useful if you intend to use multiple targets for a single action" + }, + "TABLE": { + "NAME": "Name", + "ENDPOINT": "Endpoint", + "CREATIONDATE": "Creation Date" + } + } + }, "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", @@ -1356,11 +1490,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", @@ -1370,7 +1507,8 @@ "TEXTS": "Texts and Languages", "APPEARANCE": "Appearance", "OTHER": "Other", - "STORAGE": "Storage" + "STORAGE": "Storage", + "ACTIONS": "Actions" } }, "SETTING": { @@ -1515,7 +1653,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": { diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index a03b298ec4..6855d0dcbf 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -185,6 +185,32 @@ "DESCRIPTION": "La duración de vida del token de actualización en espera es el tiempo máximo que un token de actualización puede estar sin usar." } }, + "WEB_KEYS": { + "DESCRIPTION": "Administra tus claves web OIDC para firmar y validar tokens de manera segura en tu instancia de ZITADEL.", + "TABLE": { + "TITLE": "Claves Web Activas y Futuras", + "DESCRIPTION": "Tus claves web activas y próximas. Activar una nueva clave desactivará la actual.", + "NOTE": "Nota: El endpoint JWKs OIDC devuelve una respuesta almacenable en caché (por defecto 5 min). Evita activar una clave demasiado pronto, ya que puede que aún no esté disponible en cachés y clientes.", + "ACTIVATE": "Activar la siguiente Clave Web", + "ACTIVE": "Actualmente activa", + "NEXT": "Siguiente en la cola", + "FUTURE": "Futuro", + "WARNING": "La clave web tiene menos de 5 minutos" + }, + "CREATE": { + "TITLE": "Crear nueva Clave Web", + "DESCRIPTION": "Crear una nueva clave web la añadirá a tu lista. ZITADEL usa por defecto claves RSA2048 con un algoritmo de hash SHA256.", + "KEY_TYPE": "Tipo de Clave", + "BITS": "Bits", + "HASHER": "Algoritmo de Hash", + "CURVE": "Curva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Claves Web Anteriores", + "DESCRIPTION": "Estas son tus claves web anteriores que ya no están activas.", + "DEACTIVATED_ON": "Desactivada el" + } + }, "MESSAGE_TEXTS": { "TITLE": "Textos de Mensajes", "DESCRIPTION": "Personaliza los textos de tus mensajes de correo electrónico de notificación o mensajes SMS. Si deseas desactivar algunos de los idiomas, restríngelos en la configuración de idiomas de tus instancias.", @@ -502,6 +528,114 @@ "DOWNLOAD": "Descargar", "APPLY": "Aplicar" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Acciones", + "DESCRIPTION": "Las acciones te permiten ejecutar código personalizado en respuesta a solicitudes de API, eventos o funciones específicas. Úsalas para extender Zitadel, automatizar flujos de trabajo e integrarte con otros sistemas.", + "TYPES": { + "request": "Solicitud", + "response": "Respuesta", + "events": "Eventos", + "function": "Función" + }, + "DIALOG": { + "CREATE_TITLE": "Crear una acción", + "UPDATE_TITLE": "Actualizar una acción", + "TYPE": { + "DESCRIPTION": "Selecciona cuándo quieres que se ejecute esta acción", + "REQUEST": { + "TITLE": "Solicitud", + "DESCRIPTION": "Solicitudes que ocurren dentro de Zitadel. Esto podría ser algo como una llamada de solicitud de inicio de sesión." + }, + "RESPONSE": { + "TITLE": "Respuesta", + "DESCRIPTION": "Una respuesta de una solicitud dentro de Zitadel. Piensa en la respuesta que recibes al obtener un usuario." + }, + "EVENTS": { + "TITLE": "Eventos", + "DESCRIPTION": "Eventos que ocurren dentro de Zitadel. Esto podría ser cualquier cosa como un usuario creando una cuenta, un inicio de sesión exitoso, etc." + }, + "FUNCTIONS": { + "TITLE": "Funciones", + "DESCRIPTION": "Funciones que puedes llamar dentro de Zitadel. Esto podría ser cualquier cosa, desde enviar un correo electrónico hasta crear un usuario." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Elige si esta acción se aplica a todas las solicitudes, a un servicio específico (p. ej., administración de usuarios) o a una sola solicitud (p. ej., crear usuario).", + "ALL": { + "TITLE": "Todas", + "DESCRIPTION": "Selecciona esto si quieres ejecutar tu acción en cada solicitud" + }, + "SELECT_SERVICE": { + "TITLE": "Seleccionar servicio", + "DESCRIPTION": "Elige un servicio de Zitadel para tu acción." + }, + "SELECT_METHOD": { + "TITLE": "Seleccionar método", + "DESCRIPTION": "Si quieres ejecutar solo en una solicitud específica, selecciónala aquí", + "NOTE": "Si no seleccionas un método, tu acción se ejecutará en cada solicitud de tu servicio seleccionado." + }, + "FUNCTIONNAME": { + "TITLE": "Nombre de la función", + "DESCRIPTION": "Elige la función que quieres ejecutar" + }, + "SELECT_GROUP": { + "TITLE": "Establecer grupo", + "DESCRIPTION": "Si quieres ejecutar solo en un grupo de eventos, establece el grupo aquí" + }, + "SELECT_EVENT": { + "TITLE": "Seleccionar evento", + "DESCRIPTION": "Si quieres ejecutar solo en un evento específico, especifícalo aquí" + } + }, + "TARGET": { + "DESCRIPTION": "Puedes elegir ejecutar un objetivo o ejecutarlo en las mismas condiciones que otros objetivos.", + "TARGET": { + "DESCRIPTION": "El objetivo que quieres ejecutar para esta acción" + }, + "CONDITIONS": { + "DESCRIPTION": "Condiciones de ejecución" + } + } + }, + "TABLE": { + "CONDITION": "Condición", + "TYPE": "Tipo", + "TARGET": "Objetivo", + "CREATIONDATE": "Fecha de creación" + } + }, + "TARGET": { + "TITLE": "Objetivos", + "DESCRIPTION": "Un objetivo es el destino del código que quieres ejecutar desde una acción. Crea un objetivo aquí y agrégalo a tus acciones.", + "CREATE": { + "TITLE": "Crear tu objetivo", + "DESCRIPTION": "Crea tu propio objetivo fuera de Zitadel", + "NAME": "Nombre", + "NAME_DESCRIPTION": "Dale a tu objetivo un nombre claro y descriptivo para que sea fácil de identificar más tarde", + "TYPE": "Tipo", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Llamada REST", + "restAsync": "REST Asíncrono" + }, + "ENDPOINT": "Punto de conexión", + "ENDPOINT_DESCRIPTION": "Introduce el punto de conexión donde se aloja tu código. ¡Asegúrate de que sea accesible para nosotros!", + "TIMEOUT": "Tiempo de espera", + "TIMEOUT_DESCRIPTION": "Establece el tiempo máximo que tiene tu objetivo para responder. Si tarda más, detendremos la solicitud.", + "INTERRUPT_ON_ERROR": "Interrumpir en error", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Detén todas las ejecuciones cuando los objetivos devuelvan un error", + "INTERRUPT_ON_ERROR_WARNING": "Precaución: “Interrumpir en error” detiene las operaciones si fallan, lo que puede provocar un bloqueo. Pruebe con esta opción desactivada para evitar bloquear el inicio de sesión o la creación.", + "AWAIT_RESPONSE": "Esperar respuesta", + "AWAIT_RESPONSE_DESCRIPTION": "Esperaremos una respuesta antes de hacer cualquier otra cosa. Útil si tienes la intención de usar múltiples objetivos para una sola acción" + }, + "TABLE": { + "NAME": "Nombre", + "ENDPOINT": "Punto de conexión", + "CREATIONDATE": "Fecha de creación" + } + } + }, "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", @@ -1357,6 +1491,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", @@ -1516,7 +1651,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": { diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index c63fb42cab..47acd0ac9f 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -185,6 +185,32 @@ "DESCRIPTION": "La durée de vie du token de rafraîchissement inactif est le temps maximum qu'un token de rafraîchissement peut être inutilisé." } }, + "WEB_KEYS": { + "DESCRIPTION": "Gérez vos clés Web OIDC pour signer et valider en toute sécurité les jetons de votre instance ZITADEL.", + "TABLE": { + "TITLE": "Clés Web Actives et Futures", + "DESCRIPTION": "Vos clés Web actives et à venir. L'activation d'une nouvelle clé désactivera l'actuelle.", + "NOTE": "Remarque : Le point de terminaison JWKs OIDC renvoie une réponse mise en cache (par défaut 5 min). Évitez d'activer une clé trop tôt, car elle pourrait ne pas encore être disponible pour les caches et les clients.", + "ACTIVATE": "Activer la prochaine Clé Web", + "ACTIVE": "Actuellement active", + "NEXT": "Prochaine dans la file d'attente", + "FUTURE": "Futur", + "WARNING": "La clé Web a moins de 5 minutes" + }, + "CREATE": { + "TITLE": "Créer une nouvelle Clé Web", + "DESCRIPTION": "Créer une nouvelle clé Web l'ajoutera à votre liste. ZITADEL utilise par défaut des clés RSA2048 avec un hacheur SHA256.", + "KEY_TYPE": "Type de Clé", + "BITS": "Bits", + "HASHER": "Hacheur", + "CURVE": "Courbe" + }, + "PREVIOUS_TABLE": { + "TITLE": "Clés Web Précédentes", + "DESCRIPTION": "Voici vos anciennes clés Web qui ne sont plus actives.", + "DEACTIVATED_ON": "Désactivée le" + } + }, "MESSAGE_TEXTS": { "TITLE": "Textes des Messages", "DESCRIPTION": "Personnalisez les textes de vos e-mails de notification ou messages SMS. Si vous souhaitez désactiver certaines langues, restreignez-les dans les paramètres de langue de vos instances.", @@ -502,6 +528,114 @@ "DOWNLOAD": "Télécharger", "APPLY": "Appliquer" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Actions", + "DESCRIPTION": "Les actions vous permettent d'exécuter du code personnalisé en réponse à des requêtes API, des événements ou des fonctions spécifiques. Utilisez-les pour étendre Zitadel, automatiser les flux de travail et vous intégrer à d'autres systèmes.", + "TYPES": { + "request": "Requête", + "response": "Réponse", + "events": "Événements", + "function": "Fonction" + }, + "DIALOG": { + "CREATE_TITLE": "Créer une action", + "UPDATE_TITLE": "Mettre à jour une action", + "TYPE": { + "DESCRIPTION": "Sélectionnez quand vous souhaitez que cette action s'exécute", + "REQUEST": { + "TITLE": "Requête", + "DESCRIPTION": "Requêtes qui se produisent dans Zitadel. Cela pourrait être quelque chose comme un appel de requête de connexion." + }, + "RESPONSE": { + "TITLE": "Réponse", + "DESCRIPTION": "Une réponse à une requête dans Zitadel. Pensez à la réponse que vous obtenez lorsque vous récupérez un utilisateur." + }, + "EVENTS": { + "TITLE": "Événements", + "DESCRIPTION": "Événements qui se produisent dans Zitadel. Cela pourrait être n'importe quoi, comme un utilisateur créant un compte, une connexion réussie, etc." + }, + "FUNCTIONS": { + "TITLE": "Fonctions", + "DESCRIPTION": "Fonctions que vous pouvez appeler dans Zitadel. Cela pourrait être n'importe quoi, de l'envoi d'un e-mail à la création d'un utilisateur." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Choisissez si cette action s'applique à toutes les requêtes, à un service spécifique (par exemple, la gestion des utilisateurs) ou à une seule requête (par exemple, créer un utilisateur).", + "ALL": { + "TITLE": "Tous", + "DESCRIPTION": "Sélectionnez ceci si vous souhaitez exécuter votre action sur chaque requête" + }, + "SELECT_SERVICE": { + "TITLE": "Sélectionner un service", + "DESCRIPTION": "Choisissez un service Zitadel pour votre action." + }, + "SELECT_METHOD": { + "TITLE": "Sélectionner une méthode", + "DESCRIPTION": "Si vous souhaitez exécuter uniquement sur une requête spécifique, sélectionnez-la ici", + "NOTE": "Si vous ne sélectionnez pas de méthode, votre action s'exécutera sur chaque requête de votre service sélectionné." + }, + "FUNCTIONNAME": { + "TITLE": "Nom de la fonction", + "DESCRIPTION": "Choisissez la fonction que vous souhaitez exécuter" + }, + "SELECT_GROUP": { + "TITLE": "Définir un groupe", + "DESCRIPTION": "Si vous souhaitez exécuter uniquement sur un groupe d'événements, définissez le groupe ici" + }, + "SELECT_EVENT": { + "TITLE": "Sélectionner un événement", + "DESCRIPTION": "Si vous souhaitez exécuter uniquement sur un événement spécifique, spécifiez-le ici" + } + }, + "TARGET": { + "DESCRIPTION": "Vous pouvez choisir d'exécuter une cible ou de l'exécuter dans les mêmes conditions que d'autres cibles.", + "TARGET": { + "DESCRIPTION": "La cible que vous souhaitez exécuter pour cette action" + }, + "CONDITIONS": { + "DESCRIPTION": "Conditions d'exécution" + } + } + }, + "TABLE": { + "CONDITION": "Condition", + "TYPE": "Type", + "TARGET": "Cible", + "CREATIONDATE": "Date de création" + } + }, + "TARGET": { + "TITLE": "Cibles", + "DESCRIPTION": "Une cible est la destination du code que vous souhaitez exécuter à partir d'une action. Créez une cible ici et ajoutez-la à vos actions.", + "CREATE": { + "TITLE": "Créer votre cible", + "DESCRIPTION": "Créez votre propre cible en dehors de Zitadel", + "NAME": "Nom", + "NAME_DESCRIPTION": "Donnez à votre cible un nom clair et descriptif pour la rendre facile à identifier plus tard", + "TYPE": "Type", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Appel REST", + "restAsync": "REST Asynchrone" + }, + "ENDPOINT": "Point de terminaison", + "ENDPOINT_DESCRIPTION": "Entrez le point de terminaison où votre code est hébergé. Assurez-vous qu'il nous est accessible !", + "TIMEOUT": "Délai d'attente", + "TIMEOUT_DESCRIPTION": "Définissez le temps maximal dont votre cible dispose pour répondre. Si cela prend plus de temps, nous arrêterons la requête.", + "INTERRUPT_ON_ERROR": "Interrompre en cas d'erreur", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Arrêtez toutes les exécutions lorsque les cibles renvoient une erreur", + "INTERRUPT_ON_ERROR_WARNING": "Attention : “Interrompre en cas d'erreur” arrête les opérations en cas d’échec, ce qui peut entraîner un verrouillage. Testez avec cette option désactivée pour éviter de bloquer la connexion ou la création.", + "AWAIT_RESPONSE": "Attendre une réponse", + "AWAIT_RESPONSE_DESCRIPTION": "Nous attendrons une réponse avant de faire autre chose. Utile si vous avez l'intention d'utiliser plusieurs cibles pour une seule action" + }, + "TABLE": { + "NAME": "Nom", + "ENDPOINT": "Point de terminaison", + "CREATIONDATE": "Date de création" + } + } + }, "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.", @@ -1356,6 +1490,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", @@ -1515,7 +1650,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": { diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json index 8f7a5632f6..8e1f8ad7d2 100644 --- a/console/src/assets/i18n/hu.json +++ b/console/src/assets/i18n/hu.json @@ -185,6 +185,32 @@ "DESCRIPTION": "A tétlen frissítő token élettartama az a maximális idő, ameddig a frissítő token használaton kívül maradhat." } }, + "WEB_KEYS": { + "DESCRIPTION": "Kezelje az OIDC Webkulcsokat, hogy biztonságosan aláírja és érvényesítse a tokeneket a ZITADEL példányában.", + "TABLE": { + "TITLE": "Aktív és Jövőbeli Webkulcsok", + "DESCRIPTION": "Az aktív és közelgő webkulcsai. Egy új kulcs aktiválása deaktiválja az aktuálisat.", + "NOTE": "Megjegyzés: A JWKs OIDC végpont egy gyorsítótárazható választ ad vissza (alapértelmezett: 5 perc). Kerülje a kulcs túl korai aktiválását, mivel lehet, hogy még nem érhető el a gyorsítótárakban és a klienseknél.", + "ACTIVATE": "Következő Webkulcs aktiválása", + "ACTIVE": "Jelenleg aktív", + "NEXT": "Következő a sorban", + "FUTURE": "Jövőbeli", + "WARNING": "A webkulcs kevesebb mint 5 perces" + }, + "CREATE": { + "TITLE": "Új Webkulcs létrehozása", + "DESCRIPTION": "Egy új webkulcs létrehozása hozzáadja azt a listájához. A ZITADEL alapértelmezés szerint RSA2048 kulcsokat használ SHA256 hasheléssel.", + "KEY_TYPE": "Kulcstípus", + "BITS": "Bitek", + "HASHER": "Hasher", + "CURVE": "Görbe" + }, + "PREVIOUS_TABLE": { + "TITLE": "Korábbi Webkulcsok", + "DESCRIPTION": "Ezek a korábbi webkulcsai, amelyek már nem aktívak.", + "DEACTIVATED_ON": "Deaktiválva ekkor" + } + }, "MESSAGE_TEXTS": { "TITLE": "Üzenet Szövegek", "DESCRIPTION": "Testreszabhatod az értesítési e-mailjeid vagy SMS üzeneteid szövegeit. Ha le szeretnél tiltani néhány nyelvet, korlátozd azokat az instance nyelvi beállításaiban.", @@ -502,6 +528,114 @@ "DOWNLOAD": "Letöltés", "APPLY": "Alkalmaz" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Műveletek", + "DESCRIPTION": "A műveletek lehetővé teszik egyedi kód futtatását API-kérésekre, eseményekre vagy konkrét függvényekre válaszul. Használja őket a Zitadel kiterjesztéséhez, a munkafolyamatok automatizálásához és más rendszerekkel való integrációhoz.", + "TYPES": { + "request": "Kérés", + "response": "Válasz", + "events": "Események", + "function": "Függvény" + }, + "DIALOG": { + "CREATE_TITLE": "Művelet létrehozása", + "UPDATE_TITLE": "Művelet frissítése", + "TYPE": { + "DESCRIPTION": "Válassza ki, mikor szeretné futtatni ezt a műveletet", + "REQUEST": { + "TITLE": "Kérés", + "DESCRIPTION": "A Zitadelen belül előforduló kérések. Ez lehet például egy bejelentkezési kérés hívása." + }, + "RESPONSE": { + "TITLE": "Válasz", + "DESCRIPTION": "Válasz egy Zitadelen belüli kérésre. Gondoljon arra a válaszra, amelyet egy felhasználó lekérésekor kap." + }, + "EVENTS": { + "TITLE": "Események", + "DESCRIPTION": "A Zitadelen belül zajló események. Ez bármi lehet, például egy felhasználó fiók létrehozása, sikeres bejelentkezés stb." + }, + "FUNCTIONS": { + "TITLE": "Függvények", + "DESCRIPTION": "Függvények, amelyeket a Zitadelen belül hívhat. Ez bármi lehet, az e-mail küldésétől a felhasználó létrehozásáig." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Válassza ki, hogy ez a művelet vonatkozik-e minden kérésre, egy adott szolgáltatásra (pl. felhasználókezelés) vagy egyetlen kérésre (pl. felhasználó létrehozása).", + "ALL": { + "TITLE": "Összes", + "DESCRIPTION": "Válassza ezt, ha minden kérésnél futtatni szeretné a műveletet" + }, + "SELECT_SERVICE": { + "TITLE": "Szolgáltatás kiválasztása", + "DESCRIPTION": "Válasszon egy Zitadel szolgáltatást a művelethez." + }, + "SELECT_METHOD": { + "TITLE": "Módszer kiválasztása", + "DESCRIPTION": "Ha csak egy adott kérésnél szeretne futtatni, válassza ki itt", + "NOTE": "Ha nem választ módszert, a művelet minden kérésnél futni fog a kiválasztott szolgáltatásban." + }, + "FUNCTIONNAME": { + "TITLE": "Függvénynév", + "DESCRIPTION": "Válassza ki a futtatni kívánt függvényt" + }, + "SELECT_GROUP": { + "TITLE": "Csoport beállítása", + "DESCRIPTION": "Ha csak események egy csoportján szeretne futtatni, állítsa be itt a csoportot" + }, + "SELECT_EVENT": { + "TITLE": "Esemény kiválasztása", + "DESCRIPTION": "Ha csak egy adott eseményen szeretne futtatni, adja meg itt" + } + }, + "TARGET": { + "DESCRIPTION": "Választhat, hogy futtat egy célt, vagy ugyanazokkal a feltételekkel futtatja, mint más célokat.", + "TARGET": { + "DESCRIPTION": "A cél, amelyet futtatni szeretne ehhez a művelethez" + }, + "CONDITIONS": { + "DESCRIPTION": "Végrehajtási feltételek" + } + } + }, + "TABLE": { + "CONDITION": "Feltétel", + "TYPE": "Típus", + "TARGET": "Cél", + "CREATIONDATE": "Létrehozás dátuma" + } + }, + "TARGET": { + "TITLE": "Célok", + "DESCRIPTION": "A cél annak a kódnak a célja, amelyet egy műveletből szeretne futtatni. Hozzon létre itt egy célt, és adja hozzá a műveleteihez.", + "CREATE": { + "TITLE": "Cél létrehozása", + "DESCRIPTION": "Hozza létre saját célját a Zitadelen kívül", + "NAME": "Név", + "NAME_DESCRIPTION": "Adjon a céljának egy világos, leíró nevet, hogy később könnyen azonosítható legyen", + "TYPE": "Típus", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Hívás", + "restAsync": "REST Aszinkron" + }, + "ENDPOINT": "Végpont", + "ENDPOINT_DESCRIPTION": "Adja meg azt a végpontot, ahol a kódja található. Győződjön meg arról, hogy elérhető számunkra!", + "TIMEOUT": "Időtúllépés", + "TIMEOUT_DESCRIPTION": "Állítsa be a maximális időt, amíg a céljának válaszolnia kell. Ha tovább tart, leállítjuk a kérést.", + "INTERRUPT_ON_ERROR": "Hiba esetén megszakítás", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Állítsa le az összes végrehajtást, ha a célok hibát adnak vissza", + "INTERRUPT_ON_ERROR_WARNING": "Figyelem: Az „Hiba esetén megszakítás” funkció leállítja a műveleteket hiba esetén, ami kizáráshoz vezethet. Tesztelje kikapcsolt állapotban a bejelentkezés/létrehozás blokkolásának elkerülése érdekében.", + "AWAIT_RESPONSE": "Válaszra várás", + "AWAIT_RESPONSE_DESCRIPTION": "Megvárjuk a választ, mielőtt bármi mást tennénk. Hasznos, ha több célt szeretne használni egyetlen művelethez" + }, + "TABLE": { + "NAME": "Név", + "ENDPOINT": "Végpont", + "CREATIONDATE": "Létrehozás dátuma" + } + } + }, "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", @@ -1356,6 +1490,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", @@ -1513,7 +1648,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": { diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json index aff1e59854..d5cef2c054 100644 --- a/console/src/assets/i18n/id.json +++ b/console/src/assets/i18n/id.json @@ -173,6 +173,32 @@ "DESCRIPTION": "Masa pakai token penyegaran yang menganggur adalah waktu maksimum token penyegaran tidak dapat digunakan." } }, + "WEB_KEYS": { + "DESCRIPTION": "Kelola Kunci Web OIDC Anda untuk menandatangani dan memvalidasi token dengan aman untuk instance ZITADEL Anda.", + "TABLE": { + "TITLE": "Kunci Web Aktif dan Mendatang", + "DESCRIPTION": "Kunci web Anda yang aktif dan akan datang. Mengaktifkan kunci baru akan menonaktifkan kunci yang sedang digunakan.", + "NOTE": "Catatan: Endpoint JWKs OIDC mengembalikan respons yang dapat di-cache (default 5 menit). Hindari mengaktifkan kunci terlalu cepat, karena mungkin belum tersedia di cache dan klien.", + "ACTIVATE": "Aktifkan Kunci Web Berikutnya", + "ACTIVE": "Saat ini aktif", + "NEXT": "Berikutnya dalam antrean", + "FUTURE": "Mendatang", + "WARNING": "Kunci Web berusia kurang dari 5 menit" + }, + "CREATE": { + "TITLE": "Buat Kunci Web Baru", + "DESCRIPTION": "Membuat kunci web baru akan menambahkannya ke daftar Anda. ZITADEL secara default menggunakan kunci RSA2048 dengan fungsi hash SHA256.", + "KEY_TYPE": "Jenis Kunci", + "BITS": "Bit", + "HASHER": "Hasher", + "CURVE": "Kurva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Kunci Web Sebelumnya", + "DESCRIPTION": "Ini adalah kunci web sebelumnya yang tidak lagi aktif.", + "DEACTIVATED_ON": "Dinonaktifkan pada" + } + }, "MESSAGE_TEXTS": { "TITLE": "Teks Pesan", "DESCRIPTION": "Sesuaikan teks email notifikasi atau pesan SMS Anda. Jika Anda ingin menonaktifkan beberapa bahasa, batasi bahasa tersebut di pengaturan bahasa instance Anda.", @@ -469,6 +495,114 @@ "DOWNLOAD": "Unduh", "APPLY": "Menerapkan" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Tindakan", + "DESCRIPTION": "Tindakan memungkinkan Anda menjalankan kode khusus sebagai respons terhadap permintaan API, peristiwa, atau fungsi tertentu. Gunakan ini untuk memperluas Zitadel, mengotomatiskan alur kerja, dan berintegrasi dengan sistem lain.", + "TYPES": { + "request": "Permintaan", + "response": "Respons", + "events": "Peristiwa", + "function": "Fungsi" + }, + "DIALOG": { + "CREATE_TITLE": "Buat Tindakan", + "UPDATE_TITLE": "Perbarui Tindakan", + "TYPE": { + "DESCRIPTION": "Pilih kapan Anda ingin Tindakan ini dijalankan", + "REQUEST": { + "TITLE": "Permintaan", + "DESCRIPTION": "Permintaan yang terjadi di dalam Zitadel. Ini bisa berupa sesuatu seperti panggilan permintaan login." + }, + "RESPONSE": { + "TITLE": "Respons", + "DESCRIPTION": "Respons dari permintaan di dalam Zitadel. Pikirkan respons yang Anda dapatkan kembali dari pengambilan pengguna." + }, + "EVENTS": { + "TITLE": "Peristiwa", + "DESCRIPTION": "Peristiwa yang terjadi di dalam Zitadel. Ini bisa berupa apa saja seperti pengguna membuat akun, login berhasil, dll." + }, + "FUNCTIONS": { + "TITLE": "Fungsi", + "DESCRIPTION": "Fungsi yang dapat Anda panggil di dalam Zitadel. Ini bisa berupa apa saja mulai dari mengirim email hingga membuat pengguna." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Pilih apakah tindakan ini berlaku untuk semua permintaan, layanan tertentu (mis. manajemen pengguna), atau permintaan tunggal (mis. buat pengguna).", + "ALL": { + "TITLE": "Semua", + "DESCRIPTION": "Pilih ini jika Anda ingin menjalankan tindakan Anda pada setiap permintaan" + }, + "SELECT_SERVICE": { + "TITLE": "Pilih Layanan", + "DESCRIPTION": "Pilih Layanan Zitadel untuk tindakan Anda." + }, + "SELECT_METHOD": { + "TITLE": "Pilih Metode", + "DESCRIPTION": "Jika Anda hanya ingin menjalankan pada permintaan tertentu, pilih di sini", + "NOTE": "Jika Anda tidak memilih metode, tindakan Anda akan berjalan pada setiap permintaan di layanan yang Anda pilih." + }, + "FUNCTIONNAME": { + "TITLE": "Nama Fungsi", + "DESCRIPTION": "Pilih fungsi yang ingin Anda jalankan" + }, + "SELECT_GROUP": { + "TITLE": "Tetapkan Grup", + "DESCRIPTION": "Jika Anda hanya ingin menjalankan pada grup peristiwa, tetapkan grup di sini" + }, + "SELECT_EVENT": { + "TITLE": "Pilih Peristiwa", + "DESCRIPTION": "Jika Anda hanya ingin menjalankan pada peristiwa tertentu, tentukan di sini" + } + }, + "TARGET": { + "DESCRIPTION": "Anda dapat memilih untuk menjalankan target, atau menjalankannya dengan kondisi yang sama dengan target lain.", + "TARGET": { + "DESCRIPTION": "Target yang ingin Anda jalankan untuk tindakan ini" + }, + "CONDITIONS": { + "DESCRIPTION": "Kondisi Eksekusi" + } + } + }, + "TABLE": { + "CONDITION": "Kondisi", + "TYPE": "Jenis", + "TARGET": "Target", + "CREATIONDATE": "Tanggal Pembuatan" + } + }, + "TARGET": { + "TITLE": "Target", + "DESCRIPTION": "Target adalah tujuan kode yang ingin Anda jalankan dari tindakan. Buat target di sini dan tambahkan ke tindakan Anda.", + "CREATE": { + "TITLE": "Buat Target Anda", + "DESCRIPTION": "Buat target Anda sendiri di luar Zitadel", + "NAME": "Nama", + "NAME_DESCRIPTION": "Beri target Anda nama yang jelas dan deskriptif agar mudah diidentifikasi nanti", + "TYPE": "Jenis", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Panggilan REST", + "restAsync": "REST Asinkron" + }, + "ENDPOINT": "Titik Akhir", + "ENDPOINT_DESCRIPTION": "Masukkan titik akhir tempat kode Anda dihosting. Pastikan dapat diakses oleh kami!", + "TIMEOUT": "Batas Waktu", + "TIMEOUT_DESCRIPTION": "Tetapkan waktu maksimum target Anda untuk merespons. Jika membutuhkan waktu lebih lama, kami akan menghentikan permintaan.", + "INTERRUPT_ON_ERROR": "Interupsi jika Terjadi Kesalahan", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Hentikan semua eksekusi saat target mengembalikan kesalahan", + "INTERRUPT_ON_ERROR_WARNING": "Perhatian: \"Interupsi jika Terjadi Kesalahan\" akan menghentikan operasi jika terjadi kegagalan, berisiko mengunci akses. Uji dengan opsi ini dinonaktifkan untuk mencegah pemblokiran login/pembuatan.", + "AWAIT_RESPONSE": "Tunggu Respons", + "AWAIT_RESPONSE_DESCRIPTION": "Kami akan menunggu respons sebelum melakukan hal lain. Berguna jika Anda berniat menggunakan beberapa target untuk satu tindakan" + }, + "TABLE": { + "NAME": "Nama", + "ENDPOINT": "Titik Akhir", + "CREATIONDATE": "Tanggal Pembuatan" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Memiliki kendali atas seluruh instansi, termasuk semua organisasi", "IAM_OWNER_VIEWER": "Memiliki izin untuk meninjau seluruh instansi, termasuk semua organisasi", @@ -1234,6 +1368,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", @@ -1382,7 +1517,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": { diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index f4b45b2110..a42059d11c 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -185,6 +185,32 @@ "DESCRIPTION": "La durata massima di un token di refresh inattivo è il tempo massimo in cui un token di refresh può rimanere inutilizzato." } }, + "WEB_KEYS": { + "DESCRIPTION": "Gestisci le tue chiavi Web OIDC per firmare e convalidare in modo sicuro i token per la tua istanza di ZITADEL.", + "TABLE": { + "TITLE": "Chiavi Web Attive e Future", + "DESCRIPTION": "Le tue chiavi web attive e future. L'attivazione di una nuova chiave disattiverà quella attuale.", + "NOTE": "Nota: L'endpoint JWKs OIDC restituisce una risposta memorizzabile nella cache (predefinito 5 min). Evita di attivare una chiave troppo presto, poiché potrebbe non essere ancora disponibile nelle cache e nei client.", + "ACTIVATE": "Attiva la prossima Chiave Web", + "ACTIVE": "Attualmente attiva", + "NEXT": "Prossima in coda", + "FUTURE": "Futura", + "WARNING": "La chiave web ha meno di 5 minuti" + }, + "CREATE": { + "TITLE": "Crea una nuova Chiave Web", + "DESCRIPTION": "Creare una nuova chiave web la aggiungerà alla tua lista. ZITADEL utilizza chiavi RSA2048 con hash SHA256 per impostazione predefinita.", + "KEY_TYPE": "Tipo di Chiave", + "BITS": "Bit", + "HASHER": "Hasher", + "CURVE": "Curva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Chiavi Web Precedenti", + "DESCRIPTION": "Queste sono le tue chiavi web precedenti che non sono più attive.", + "DEACTIVATED_ON": "Disattivata il" + } + }, "MESSAGE_TEXTS": { "TITLE": "Testi dei Messaggi", "DESCRIPTION": "Personalizza i testi delle tue email di notifica o messaggi SMS. Se vuoi disabilitare alcune lingue, limitale nelle impostazioni lingua delle tue istanze.", @@ -501,6 +527,114 @@ "DOWNLOAD": "Scarica", "APPLY": "Applicare" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Azioni", + "DESCRIPTION": "Le azioni consentono di eseguire codice personalizzato in risposta a richieste API, eventi o funzioni specifiche. Usale per estendere Zitadel, automatizzare i flussi di lavoro e integrarti con altri sistemi.", + "TYPES": { + "request": "Richiesta", + "response": "Risposta", + "events": "Eventi", + "function": "Funzione" + }, + "DIALOG": { + "CREATE_TITLE": "Crea un'azione", + "UPDATE_TITLE": "Aggiorna un'azione", + "TYPE": { + "DESCRIPTION": "Seleziona quando vuoi che venga eseguita questa azione", + "REQUEST": { + "TITLE": "Richiesta", + "DESCRIPTION": "Richieste che si verificano all'interno di Zitadel. Potrebbe trattarsi di una chiamata di richiesta di accesso." + }, + "RESPONSE": { + "TITLE": "Risposta", + "DESCRIPTION": "Una risposta a una richiesta all'interno di Zitadel. Pensa alla risposta che ricevi quando recuperi un utente." + }, + "EVENTS": { + "TITLE": "Eventi", + "DESCRIPTION": "Eventi che si verificano all'interno di Zitadel. Potrebbe trattarsi di qualsiasi cosa, come un utente che crea un account, un accesso riuscito, ecc." + }, + "FUNCTIONS": { + "TITLE": "Funzioni", + "DESCRIPTION": "Funzioni che puoi chiamare all'interno di Zitadel. Potrebbe trattarsi di qualsiasi cosa, dall'invio di un'e-mail alla creazione di un utente." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Scegli se questa azione si applica a tutte le richieste, a un servizio specifico (ad es. gestione utenti) o a una singola richiesta (ad es. crea utente).", + "ALL": { + "TITLE": "Tutte", + "DESCRIPTION": "Seleziona questa opzione se vuoi eseguire la tua azione su ogni richiesta" + }, + "SELECT_SERVICE": { + "TITLE": "Seleziona servizio", + "DESCRIPTION": "Scegli un servizio Zitadel per la tua azione." + }, + "SELECT_METHOD": { + "TITLE": "Seleziona metodo", + "DESCRIPTION": "Se vuoi eseguire solo su una richiesta specifica, selezionala qui", + "NOTE": "Se non selezioni un metodo, la tua azione verrà eseguita su ogni richiesta nel servizio selezionato." + }, + "FUNCTIONNAME": { + "TITLE": "Nome funzione", + "DESCRIPTION": "Scegli la funzione che vuoi eseguire" + }, + "SELECT_GROUP": { + "TITLE": "Imposta gruppo", + "DESCRIPTION": "Se vuoi eseguire solo su un gruppo di eventi, imposta il gruppo qui" + }, + "SELECT_EVENT": { + "TITLE": "Seleziona evento", + "DESCRIPTION": "Se vuoi eseguire solo su un evento specifico, specificalo qui" + } + }, + "TARGET": { + "DESCRIPTION": "Puoi scegliere di eseguire un obiettivo o di eseguirlo alle stesse condizioni di altri obiettivi.", + "TARGET": { + "DESCRIPTION": "L'obiettivo che vuoi eseguire per questa azione" + }, + "CONDITIONS": { + "DESCRIPTION": "Condizioni di esecuzione" + } + } + }, + "TABLE": { + "CONDITION": "Condizione", + "TYPE": "Tipo", + "TARGET": "Obiettivo", + "CREATIONDATE": "Data di creazione" + } + }, + "TARGET": { + "TITLE": "Obiettivi", + "DESCRIPTION": "Un obiettivo è la destinazione del codice che vuoi eseguire da un'azione. Crea un obiettivo qui e aggiungilo alle tue azioni.", + "CREATE": { + "TITLE": "Crea il tuo obiettivo", + "DESCRIPTION": "Crea il tuo obiettivo al di fuori di Zitadel", + "NAME": "Nome", + "NAME_DESCRIPTION": "Dai al tuo obiettivo un nome chiaro e descrittivo per renderlo facile da identificare in seguito", + "TYPE": "Tipo", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Chiamata REST", + "restAsync": "REST Asincrono" + }, + "ENDPOINT": "Endpoint", + "ENDPOINT_DESCRIPTION": "Inserisci l'endpoint in cui è ospitato il tuo codice. Assicurati che sia accessibile per noi!", + "TIMEOUT": "Timeout", + "TIMEOUT_DESCRIPTION": "Imposta il tempo massimo che il tuo obiettivo ha per rispondere. Se impiega più tempo, interromperemo la richiesta.", + "INTERRUPT_ON_ERROR": "Interrompi in caso di errore", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Interrompi tutte le esecuzioni quando gli obiettivi restituiscono un errore", + "INTERRUPT_ON_ERROR_WARNING": "Attenzione: “Interrompi in caso di errore” arresta le operazioni in caso di fallimento, rischiando il blocco. Testare con l’opzione disattivata per evitare il blocco dell’accesso/creazione.", + "AWAIT_RESPONSE": "Attendi risposta", + "AWAIT_RESPONSE_DESCRIPTION": "Aspetteremo una risposta prima di fare altro. Utile se intendi utilizzare più obiettivi per una singola azione" + }, + "TABLE": { + "NAME": "Nome", + "ENDPOINT": "Endpoint", + "CREATIONDATE": "Data di creazione" + } + } + }, "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", @@ -1356,6 +1490,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", @@ -1515,7 +1650,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": { diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index d323b961d2..8a9574b7d9 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -185,6 +185,32 @@ "DESCRIPTION": "アイドル状態のリフレッシュトークンの有効期間は、リフレッシュトークンが使用されない最大時間です。" } }, + "WEB_KEYS": { + "DESCRIPTION": "ZITADELインスタンスのトークンを安全に署名および検証するために、OIDC Webキーを管理します。", + "TABLE": { + "TITLE": "アクティブおよび今後のWebキー", + "DESCRIPTION": "現在アクティブなWebキーと、今後使用予定のWebキーです。新しいキーをアクティブ化すると、現在のキーは無効になります。", + "NOTE": "注意: JWKs OIDCエンドポイントはキャッシュ可能なレスポンスを返します(デフォルト5分)。キーを早くアクティブ化しすぎると、キャッシュやクライアントでまだ利用できない可能性があります。", + "ACTIVATE": "次のWebキーをアクティブ化", + "ACTIVE": "現在アクティブ", + "NEXT": "次のキュー", + "FUTURE": "今後", + "WARNING": "ウェブキーは5分未満です。" + }, + "CREATE": { + "TITLE": "新しいWebキーを作成", + "DESCRIPTION": "新しいWebキーを作成すると、リストに追加されます。ZITADELはデフォルトでRSA2048キーとSHA256ハッシュを使用します。", + "KEY_TYPE": "キーの種類", + "BITS": "ビット", + "HASHER": "ハッシュ方式", + "CURVE": "カーブ" + }, + "PREVIOUS_TABLE": { + "TITLE": "以前のWebキー", + "DESCRIPTION": "これらは、すでに無効になった以前のWebキーです。", + "DEACTIVATED_ON": "無効化日" + } + }, "MESSAGE_TEXTS": { "TITLE": "メッセージテキスト", "DESCRIPTION": "通知メールやSMSメッセージのテキストをカスタマイズします。一部の言語を無効にしたい場合は、インスタンスの言語設定で制限してください。", @@ -502,6 +528,114 @@ "DOWNLOAD": "ダウンロード", "APPLY": "アプライ" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "アクション", + "DESCRIPTION": "アクションを使用すると、APIリクエスト、イベント、または特定の関数に応答してカスタムコードを実行できます。これらを使用して、Zitadelを拡張し、ワークフローを自動化し、他のシステムと統合します。", + "TYPES": { + "request": "リクエスト", + "response": "レスポンス", + "events": "イベント", + "function": "関数" + }, + "DIALOG": { + "CREATE_TITLE": "アクションを作成", + "UPDATE_TITLE": "アクションを更新", + "TYPE": { + "DESCRIPTION": "このアクションを実行するタイミングを選択します", + "REQUEST": { + "TITLE": "リクエスト", + "DESCRIPTION": "Zitadel内で発生するリクエスト。これはログインリクエストの呼び出しのようなものです。" + }, + "RESPONSE": { + "TITLE": "レスポンス", + "DESCRIPTION": "Zitadel内のリクエストからのレスポンス。ユーザーのフェッチから返されるレスポンスを考えてください。" + }, + "EVENTS": { + "TITLE": "イベント", + "DESCRIPTION": "Zitadel内で発生するイベント。これは、ユーザーアカウントの作成、ログインの成功など、あらゆる可能性があります。" + }, + "FUNCTIONS": { + "TITLE": "関数", + "DESCRIPTION": "Zitadel内で呼び出すことができる関数。これは、電子メールの送信からユーザーの作成まで、あらゆる可能性があります。" + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "このアクションをすべてのリクエスト、特定のサービス(例:ユーザー管理)、または単一のリクエスト(例:ユーザーの作成)に適用するかどうかを選択します。", + "ALL": { + "TITLE": "すべて", + "DESCRIPTION": "すべてのリクエストでアクションを実行する場合は、これを選択します" + }, + "SELECT_SERVICE": { + "TITLE": "サービスを選択", + "DESCRIPTION": "アクションのZitadelサービスを選択します。" + }, + "SELECT_METHOD": { + "TITLE": "メソッドを選択", + "DESCRIPTION": "特定のリクエストでのみ実行する場合は、ここで選択します", + "NOTE": "メソッドを選択しない場合、アクションは選択したサービスのすべてのリクエストで実行されます。" + }, + "FUNCTIONNAME": { + "TITLE": "関数名", + "DESCRIPTION": "実行する関数を選択します" + }, + "SELECT_GROUP": { + "TITLE": "グループを設定", + "DESCRIPTION": "イベントのグループでのみ実行する場合は、ここでグループを設定します" + }, + "SELECT_EVENT": { + "TITLE": "イベントを選択", + "DESCRIPTION": "特定のイベントでのみ実行する場合は、ここで指定します" + } + }, + "TARGET": { + "DESCRIPTION": "ターゲットを実行するか、他のターゲットと同じ条件で実行するかを選択できます。", + "TARGET": { + "DESCRIPTION": "このアクションで実行するターゲット" + }, + "CONDITIONS": { + "DESCRIPTION": "実行条件" + } + } + }, + "TABLE": { + "CONDITION": "条件", + "TYPE": "タイプ", + "TARGET": "ターゲット", + "CREATIONDATE": "作成日" + } + }, + "TARGET": { + "TITLE": "ターゲット", + "DESCRIPTION": "ターゲットは、アクションから実行するコードの宛先です。ここでターゲットを作成し、アクションに追加します。", + "CREATE": { + "TITLE": "ターゲットを作成", + "DESCRIPTION": "Zitadelの外部で独自のターゲットを作成します", + "NAME": "名前", + "NAME_DESCRIPTION": "後で簡単に識別できるように、ターゲットに明確でわかりやすい名前を付けます", + "TYPE": "タイプ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST 呼び出し", + "restAsync": "REST 非同期" + }, + "ENDPOINT": "エンドポイント", + "ENDPOINT_DESCRIPTION": "コードがホストされているエンドポイントを入力します。アクセス可能であることを確認してください。", + "TIMEOUT": "タイムアウト", + "TIMEOUT_DESCRIPTION": "ターゲットが応答する最大時間を設定します。これより時間がかかる場合は、リクエストを停止します。", + "INTERRUPT_ON_ERROR": "エラー時に中断", + "INTERRUPT_ON_ERROR_DESCRIPTION": "ターゲットがエラーを返した場合、すべての実行を停止します", + "INTERRUPT_ON_ERROR_WARNING": "注意:「エラー時に中断」を有効にすると、失敗時に処理が停止し、ロックアウトのリスクがあります。ログイン/作成のブロックを防ぐため、無効にしてテストしてください。", + "AWAIT_RESPONSE": "レスポンスを待機", + "AWAIT_RESPONSE_DESCRIPTION": "他の処理を行う前にレスポンスを待機します。単一のアクションに複数のターゲットを使用する場合に便利です" + }, + "TABLE": { + "NAME": "名前", + "ENDPOINT": "エンドポイント", + "CREATIONDATE": "作成日" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "すべての組織を含むインスタンス全体を管理する権限を持ちます", "IAM_OWNER_VIEWER": "すべての組織を含むインスタンス全体を閲覧する権限を持ちます", @@ -1356,6 +1490,7 @@ "BRANDING": "ブランディング", "PRIVACYPOLICY": "プライバシーポリシー", "OIDC": "OIDCトークンのライフタイムと有効期限", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "シークレット設定", "SECURITY": "セキュリティ設定", "EVENTS": "イベント", @@ -1515,7 +1650,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": { diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json index e6184cb8d1..c596b3b067 100644 --- a/console/src/assets/i18n/ko.json +++ b/console/src/assets/i18n/ko.json @@ -185,6 +185,32 @@ "DESCRIPTION": "유휴 갱신 토큰 수명은 갱신 토큰이 사용되지 않는 최대 기간을 의미합니다." } }, + "WEB_KEYS": { + "DESCRIPTION": "ZITADEL 인스턴스의 토큰을 안전하게 서명하고 검증하기 위해 OIDC 웹 키를 관리하세요.", + "TABLE": { + "TITLE": "활성 및 예정된 웹 키", + "DESCRIPTION": "현재 활성화된 웹 키와 앞으로 활성화될 웹 키입니다. 새로운 키를 활성화하면 기존 키는 비활성화됩니다.", + "NOTE": "참고: JWKs OIDC 엔드포인트는 캐시 가능한 응답을 반환합니다 (기본값: 5분). 키를 너무 빨리 활성화하면 캐시 및 클라이언트에서 아직 사용할 수 없을 수 있습니다.", + "ACTIVATE": "다음 웹 키 활성화", + "ACTIVE": "현재 활성화됨", + "NEXT": "대기 중인 다음 키", + "FUTURE": "향후 사용 예정", + "WARNING": "웹 키가 5분 미만입니다." + }, + "CREATE": { + "TITLE": "새 웹 키 생성", + "DESCRIPTION": "새 웹 키를 생성하면 목록에 추가됩니다. ZITADEL은 기본적으로 RSA2048 키와 SHA256 해시 알고리즘을 사용합니다.", + "KEY_TYPE": "키 유형", + "BITS": "비트", + "HASHER": "해시 알고리즘", + "CURVE": "곡선" + }, + "PREVIOUS_TABLE": { + "TITLE": "이전 웹 키", + "DESCRIPTION": "더 이상 활성 상태가 아닌 이전 웹 키 목록입니다.", + "DEACTIVATED_ON": "비활성화된 날짜" + } + }, "MESSAGE_TEXTS": { "TITLE": "메시지 텍스트", "DESCRIPTION": "알림 이메일 또는 SMS 메시지의 텍스트를 사용자 정의하세요. 언어를 비활성화하려면 인스턴스의 언어 설정에서 제한하세요.", @@ -502,6 +528,114 @@ "DOWNLOAD": "다운로드", "APPLY": "적용" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "작업", + "DESCRIPTION": "작업을 통해 API 요청, 이벤트 또는 특정 함수에 대한 응답으로 사용자 지정 코드를 실행할 수 있습니다. 이를 사용하여 Zitadel을 확장하고 워크플로를 자동화하며 다른 시스템과 통합합니다.", + "TYPES": { + "request": "요청", + "response": "응답", + "events": "이벤트", + "function": "함수" + }, + "DIALOG": { + "CREATE_TITLE": "작업 생성", + "UPDATE_TITLE": "작업 업데이트", + "TYPE": { + "DESCRIPTION": "이 작업을 실행할 시점을 선택하십시오.", + "REQUEST": { + "TITLE": "요청", + "DESCRIPTION": "Zitadel 내에서 발생하는 요청. 이는 로그인 요청 호출과 같은 것일 수 있습니다." + }, + "RESPONSE": { + "TITLE": "응답", + "DESCRIPTION": "Zitadel 내 요청으로부터의 응답. 사용자를 가져올 때 받는 응답을 생각해 보십시오." + }, + "EVENTS": { + "TITLE": "이벤트", + "DESCRIPTION": "Zitadel 내에서 발생하는 이벤트. 이는 사용자 계정 생성, 로그인 성공 등 모든 것이 될 수 있습니다." + }, + "FUNCTIONS": { + "TITLE": "함수", + "DESCRIPTION": "Zitadel 내에서 호출할 수 있는 함수입니다. 이는 이메일 전송부터 사용자 생성까지 모든 것이 될 수 있습니다." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "이 작업이 모든 요청, 특정 서비스(예: 사용자 관리) 또는 단일 요청(예: 사용자 생성)에 적용되는지 선택하십시오.", + "ALL": { + "TITLE": "모두", + "DESCRIPTION": "모든 요청에서 작업을 실행하려면 이것을 선택하십시오." + }, + "SELECT_SERVICE": { + "TITLE": "서비스 선택", + "DESCRIPTION": "작업에 대한 Zitadel 서비스를 선택하십시오." + }, + "SELECT_METHOD": { + "TITLE": "메서드 선택", + "DESCRIPTION": "특정 요청에서만 실행하려면 여기에서 선택하십시오.", + "NOTE": "메서드를 선택하지 않으면 선택한 서비스의 모든 요청에서 작업이 실행됩니다." + }, + "FUNCTIONNAME": { + "TITLE": "함수 이름", + "DESCRIPTION": "실행할 함수를 선택하십시오." + }, + "SELECT_GROUP": { + "TITLE": "그룹 설정", + "DESCRIPTION": "이벤트 그룹에서만 실행하려면 여기에서 그룹을 설정하십시오." + }, + "SELECT_EVENT": { + "TITLE": "이벤트 선택", + "DESCRIPTION": "특정 이벤트에서만 실행하려면 여기에서 지정하십시오." + } + }, + "TARGET": { + "DESCRIPTION": "타겟을 실행하거나 다른 타겟과 동일한 조건으로 실행하도록 선택할 수 있습니다.", + "TARGET": { + "DESCRIPTION": "이 작업에 대해 실행할 타겟" + }, + "CONDITIONS": { + "DESCRIPTION": "실행 조건" + } + } + }, + "TABLE": { + "CONDITION": "조건", + "TYPE": "유형", + "TARGET": "타겟", + "CREATIONDATE": "생성 날짜" + } + }, + "TARGET": { + "TITLE": "타겟", + "DESCRIPTION": "타겟은 작업에서 실행하려는 코드의 대상입니다. 여기에서 타겟을 생성하고 작업에 추가하십시오.", + "CREATE": { + "TITLE": "타겟 생성", + "DESCRIPTION": "Zitadel 외부에서 자체 타겟을 생성하십시오.", + "NAME": "이름", + "NAME_DESCRIPTION": "나중에 쉽게 식별할 수 있도록 타겟에 명확하고 설명적인 이름을 지정하십시오.", + "TYPE": "유형", + "TYPES": { + "restWebhook": "REST 웹훅", + "restCall": "REST 호출", + "restAsync": "REST 비동기" + }, + "ENDPOINT": "엔드포인트", + "ENDPOINT_DESCRIPTION": "코드가 호스팅되는 엔드포인트를 입력하십시오. 우리에게 액세스할 수 있는지 확인하십시오!", + "TIMEOUT": "시간 초과", + "TIMEOUT_DESCRIPTION": "타겟이 응답해야 하는 최대 시간을 설정하십시오. 시간이 더 오래 걸리면 요청을 중지합니다.", + "INTERRUPT_ON_ERROR": "오류 시 중단", + "INTERRUPT_ON_ERROR_DESCRIPTION": "타겟이 오류를 반환하면 모든 실행을 중지하십시오.", + "INTERRUPT_ON_ERROR_WARNING": "주의: “오류 시 중단” 기능은 실패 시 작업을 중단하며, 잠금 위험이 있습니다. 로그인/생성 차단을 방지하려면 비활성화된 상태로 테스트하세요.", + "AWAIT_RESPONSE": "응답 대기", + "AWAIT_RESPONSE_DESCRIPTION": "다른 작업을 수행하기 전에 응답을 기다립니다. 단일 작업에 여러 타겟을 사용하려는 경우 유용합니다." + }, + "TABLE": { + "NAME": "이름", + "ENDPOINT": "엔드포인트", + "CREATIONDATE": "생성 날짜" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "인스턴스와 모든 조직에 대한 제어 권한이 있습니다", "IAM_OWNER_VIEWER": "인스턴스와 모든 조직을 검토할 수 있는 권한이 있습니다", @@ -1356,6 +1490,7 @@ "BRANDING": "브랜딩", "PRIVACYPOLICY": "외부 링크", "OIDC": "OIDC 토큰 수명 및 만료", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "시크릿 생성기", "SECURITY": "보안 설정", "EVENTS": "이벤트", @@ -1515,7 +1650,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": { diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 9002ff2534..c01b485f0a 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -185,6 +185,32 @@ "DESCRIPTION": "Животниот век на неактивниот токен за освежување е максималното време кое токен за освежување може да не се користи." } }, + "WEB_KEYS": { + "DESCRIPTION": "Управувајте со вашите OIDC веб-клучеви за безбедно потпишување и валидација на токени за вашата ZITADEL инстанца.", + "TABLE": { + "TITLE": "Активни и Идни Веб-Клучеви", + "DESCRIPTION": "Вашите активни и претстојни веб-клучеви. Активирањето на нов клуч ќе го деактивира тековниот.", + "NOTE": "Забелешка: JWKs OIDC крајната точка враќа одговор што може да се кешира (стандардно 5 минути). Избегнувајте активирање на клучот пребрзо, бидејќи можеби сè уште не е достапен во кешот и кај клиентите.", + "ACTIVATE": "Активирај го следниот веб-клуч", + "ACTIVE": "Моментално активен", + "NEXT": "Следен во редот", + "FUTURE": "Иднина", + "WARNING": "Веб-клучот е помалку од 5 минути стар" + }, + "CREATE": { + "TITLE": "Креирај нов веб-клуч", + "DESCRIPTION": "Креирањето нов веб-клуч го додава на вашата листа. ZITADEL стандардно користи RSA2048 клучеви со SHA256 алгоритам за хаширање.", + "KEY_TYPE": "Тип на клуч", + "BITS": "Битови", + "HASHER": "Алгоритам за хаширање", + "CURVE": "Крива" + }, + "PREVIOUS_TABLE": { + "TITLE": "Претходни веб-клучеви", + "DESCRIPTION": "Ова се вашите претходни веб-клучеви кои повеќе не се активни.", + "DEACTIVATED_ON": "Деактивиран на" + } + }, "MESSAGE_TEXTS": { "TITLE": "Текстови на пораки", "DESCRIPTION": "Прилагодете ги текстовите на вашите е-маил или SMS пораки за известување. Ако сакате да оневозможите некои јазици, ограничете ги во поставките за јазик на вашите инстанци.", @@ -502,6 +528,114 @@ "DOWNLOAD": "Преземи", "APPLY": "Пријавете се" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Акции", + "DESCRIPTION": "Акциите ви овозможуваат да извршувате прилагоден код како одговор на API барања, настани или специфични функции. Користете ги за да го проширите Zitadel, да ги автоматизирате работните процеси и да се интегрирате со други системи.", + "TYPES": { + "request": "Барање", + "response": "Одговор", + "events": "Настани", + "function": "Функција" + }, + "DIALOG": { + "CREATE_TITLE": "Креирај акција", + "UPDATE_TITLE": "Ажурирај акција", + "TYPE": { + "DESCRIPTION": "Изберете кога сакате да се изврши оваа акција", + "REQUEST": { + "TITLE": "Барање", + "DESCRIPTION": "Барања што се случуваат во Zitadel. Ова може да биде нешто како повик за барање за најава." + }, + "RESPONSE": { + "TITLE": "Одговор", + "DESCRIPTION": "Одговор од барање во Zitadel. Размислете за одговорот што го добивате при преземање на корисник." + }, + "EVENTS": { + "TITLE": "Настани", + "DESCRIPTION": "Настани што се случуваат во Zitadel. Ова може да биде нешто како корисник што креира сметка, успешна најава итн." + }, + "FUNCTIONS": { + "TITLE": "Функции", + "DESCRIPTION": "Функции што можете да ги повикате во Zitadel. Ова може да биде сè, од испраќање е-пошта до креирање корисник." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Изберете дали оваа акција се однесува на сите барања, одредена услуга (на пр. управување со корисници) или едно барање (на пр. креирај корисник).", + "ALL": { + "TITLE": "Сите", + "DESCRIPTION": "Изберете го ова ако сакате да ја извршите вашата акција на секое барање" + }, + "SELECT_SERVICE": { + "TITLE": "Изберете услуга", + "DESCRIPTION": "Изберете Zitadel услуга за вашата акција." + }, + "SELECT_METHOD": { + "TITLE": "Изберете метод", + "DESCRIPTION": "Ако сакате да извршите само на одредено барање, изберете го тука", + "NOTE": "Ако не изберете метод, вашата акција ќе се изврши на секое барање во вашата избрана услуга." + }, + "FUNCTIONNAME": { + "TITLE": "Име на функција", + "DESCRIPTION": "Изберете ја функцијата што сакате да ја извршите" + }, + "SELECT_GROUP": { + "TITLE": "Постави група", + "DESCRIPTION": "Ако сакате да извршите само на група настани, поставете ја групата тука" + }, + "SELECT_EVENT": { + "TITLE": "Изберете настан", + "DESCRIPTION": "Ако сакате да извршите само на одреден настан, наведете го тука" + } + }, + "TARGET": { + "DESCRIPTION": "Можете да изберете да извршите цел или да ја извршите под истите услови како и другите цели.", + "TARGET": { + "DESCRIPTION": "Целта што сакате да ја извршите за оваа акција" + }, + "CONDITIONS": { + "DESCRIPTION": "Услови за извршување" + } + } + }, + "TABLE": { + "CONDITION": "Услов", + "TYPE": "Тип", + "TARGET": "Цел", + "CREATIONDATE": "Датум на создавање" + } + }, + "TARGET": { + "TITLE": "Цели", + "DESCRIPTION": "Целта е дестинација на кодот што сакате да го извршите од акција. Креирајте цел овде и додајте ја на вашите акции.", + "CREATE": { + "TITLE": "Креирајте ја вашата цел", + "DESCRIPTION": "Креирајте ја вашата сопствена цел надвор од Zitadel", + "NAME": "Име", + "NAME_DESCRIPTION": "Дајте ѝ на вашата цел јасно, опис на име за да биде лесно да се идентификува подоцна", + "TYPE": "Тип", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Повик", + "restAsync": "REST Асинхроно" + }, + "ENDPOINT": "Крајна точка", + "ENDPOINT_DESCRIPTION": "Внесете ја крајната точка каде што е хостиран вашиот код. Осигурете се дека е достапна за нас!", + "TIMEOUT": "Време на истекување", + "TIMEOUT_DESCRIPTION": "Поставете го максималното време што вашата цел треба да одговори. Ако трае подолго, ќе го запреме барањето.", + "INTERRUPT_ON_ERROR": "Прекини при грешка", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Запрете ги сите извршувања кога целите ќе вратат грешка", + "INTERRUPT_ON_ERROR_WARNING": "Внимание: „Прекини при грешка“ ги запира операциите при неуспех, со ризик од блокирање. Тестирајте со исклучена опција за да избегнете блокирање на најавата/креирањето.", + "AWAIT_RESPONSE": "Почекај одговор", + "AWAIT_RESPONSE_DESCRIPTION": "Ќе почекаме одговор пред да направиме нешто друго. Корисно ако планирате да користите повеќе цели за една акција" + }, + "TABLE": { + "NAME": "Име", + "ENDPOINT": "Крајна точка", + "CREATIONDATE": "Датум на создавање" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Има контрола врз целата инстанца, вклучувајќи ги сите организации", "IAM_OWNER_VIEWER": "Има дозвола за преглед на целата инстанца, вклучувајќи ги сите организации", @@ -1357,6 +1491,7 @@ "BRANDING": "Брендирање", "PRIVACYPOLICY": "Политика за приватност", "OIDC": "OIDC времетраење и истекување на токени", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Изглед на тајни", "SECURITY": "Подесувања за безбедност", "EVENTS": "Настани", @@ -1516,7 +1651,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": { diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index 04e3174166..ab8243e2a5 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -185,6 +185,32 @@ "DESCRIPTION": "De levensduur van het inactieve vernieuwingstoken is de maximale tijd dat een vernieuwingstoken ongebruikt kan zijn." } }, + "WEB_KEYS": { + "DESCRIPTION": "Beheer je OIDC Web Keys om tokens veilig te ondertekenen en te valideren voor je ZITADEL-instantie.", + "TABLE": { + "TITLE": "Actieve en Toekomstige Websleutels", + "DESCRIPTION": "Je actieve en aankomende websleutels. Het activeren van een nieuwe sleutel deactiveert de huidige.", + "NOTE": "Opmerking: Het JWKs OIDC-eindpunt geeft een cachebare respons terug (standaard 5 minuten). Vermijd het te vroeg activeren van een sleutel, omdat deze mogelijk nog niet beschikbaar is in caches en bij clients.", + "ACTIVATE": "Volgende Websleutel activeren", + "ACTIVE": "Momenteel actief", + "NEXT": "Volgende in de wachtrij", + "FUTURE": "Toekomstig", + "WARNING": "De websleutel is minder dan 5 minuten oud" + }, + "CREATE": { + "TITLE": "Nieuwe Websleutel aanmaken", + "DESCRIPTION": "Het aanmaken van een nieuwe websleutel voegt deze toe aan je lijst. ZITADEL gebruikt standaard RSA2048-sleutels met een SHA256-hasher.", + "KEY_TYPE": "Sleuteltype", + "BITS": "Bits", + "HASHER": "Hasher", + "CURVE": "Curve" + }, + "PREVIOUS_TABLE": { + "TITLE": "Vorige Websleutels", + "DESCRIPTION": "Dit zijn je vorige websleutels die niet langer actief zijn.", + "DEACTIVATED_ON": "Gedeactiveerd op" + } + }, "MESSAGE_TEXTS": { "TITLE": "Berichtteksten", "DESCRIPTION": "Pas de teksten van je notificatie-e-mail of SMS-berichten aan. Als je sommige talen wilt uitschakelen, beperk ze dan in de taalinstellingen van je instanties.", @@ -502,6 +528,114 @@ "DOWNLOAD": "Download", "APPLY": "Toepassen" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Acties", + "DESCRIPTION": "Met acties kunt u aangepaste code uitvoeren als reactie op API-verzoeken, gebeurtenissen of specifieke functies. Gebruik ze om Zitadel uit te breiden, workflows te automatiseren en te integreren met andere systemen.", + "TYPES": { + "request": "Verzoek", + "response": "Reactie", + "events": "Gebeurtenissen", + "function": "Functie" + }, + "DIALOG": { + "CREATE_TITLE": "Een actie maken", + "UPDATE_TITLE": "Een actie bijwerken", + "TYPE": { + "DESCRIPTION": "Selecteer wanneer u deze actie wilt uitvoeren", + "REQUEST": { + "TITLE": "Verzoek", + "DESCRIPTION": "Verzoeken die binnen Zitadel plaatsvinden. Dit kan zoiets zijn als een inlogverzoek-oproep." + }, + "RESPONSE": { + "TITLE": "Reactie", + "DESCRIPTION": "Een reactie op een verzoek binnen Zitadel. Denk aan de reactie die u terugkrijgt van het ophalen van een gebruiker." + }, + "EVENTS": { + "TITLE": "Gebeurtenissen", + "DESCRIPTION": "Gebeurtenissen die binnen Zitadel plaatsvinden. Dit kan van alles zijn, zoals een gebruiker die een account aanmaakt, een succesvolle login enz." + }, + "FUNCTIONS": { + "TITLE": "Functies", + "DESCRIPTION": "Functies die u binnen Zitadel kunt aanroepen. Dit kan van alles zijn, van het verzenden van een e-mail tot het maken van een gebruiker." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Kies of deze actie van toepassing is op alle verzoeken, een specifieke service (bijv. gebruikersbeheer) of een enkel verzoek (bijv. gebruiker maken).", + "ALL": { + "TITLE": "Alle", + "DESCRIPTION": "Selecteer dit als u uw actie bij elk verzoek wilt uitvoeren" + }, + "SELECT_SERVICE": { + "TITLE": "Service selecteren", + "DESCRIPTION": "Kies een Zitadel-service voor uw actie." + }, + "SELECT_METHOD": { + "TITLE": "Methode selecteren", + "DESCRIPTION": "Als u alleen bij een specifiek verzoek wilt uitvoeren, selecteert u dit hier", + "NOTE": "Als u geen methode selecteert, wordt uw actie bij elk verzoek in uw geselecteerde service uitgevoerd." + }, + "FUNCTIONNAME": { + "TITLE": "Functienaam", + "DESCRIPTION": "Kies de functie die u wilt uitvoeren" + }, + "SELECT_GROUP": { + "TITLE": "Groep instellen", + "DESCRIPTION": "Als u alleen bij een groep gebeurtenissen wilt uitvoeren, stelt u de groep hier in" + }, + "SELECT_EVENT": { + "TITLE": "Gebeurtenis selecteren", + "DESCRIPTION": "Als u alleen bij een specifieke gebeurtenis wilt uitvoeren, specificeert u deze hier" + } + }, + "TARGET": { + "DESCRIPTION": "U kunt ervoor kiezen om een doel uit te voeren of om het onder dezelfde voorwaarden als andere doelen uit te voeren.", + "TARGET": { + "DESCRIPTION": "Het doel dat u voor deze actie wilt uitvoeren" + }, + "CONDITIONS": { + "DESCRIPTION": "Uitvoeringsvoorwaarden" + } + } + }, + "TABLE": { + "CONDITION": "Voorwaarde", + "TYPE": "Type", + "TARGET": "Doel", + "CREATIONDATE": "Aanmaakdatum" + } + }, + "TARGET": { + "TITLE": "Doelen", + "DESCRIPTION": "Een doel is de bestemming van de code die u vanuit een actie wilt uitvoeren. Maak hier een doel en voeg het toe aan uw acties.", + "CREATE": { + "TITLE": "Uw doel maken", + "DESCRIPTION": "Maak uw eigen doel buiten Zitadel", + "NAME": "Naam", + "NAME_DESCRIPTION": "Geef uw doel een duidelijke, beschrijvende naam om het later gemakkelijk te kunnen identificeren", + "TYPE": "Type", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Aanroep", + "restAsync": "REST Asynchroon" + }, + "ENDPOINT": "Eindpunt", + "ENDPOINT_DESCRIPTION": "Voer het eindpunt in waar uw code wordt gehost. Zorg ervoor dat het voor ons toegankelijk is!", + "TIMEOUT": "Time-out", + "TIMEOUT_DESCRIPTION": "Stel de maximale tijd in die uw doel heeft om te reageren. Als het langer duurt, stoppen we het verzoek.", + "INTERRUPT_ON_ERROR": "Onderbreken bij fout", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Stop alle uitvoeringen als de doelen een fout retourneren", + "INTERRUPT_ON_ERROR_WARNING": "Let op: “Onderbreken bij fout” stopt operaties bij een mislukking, met kans op blokkering. Test met deze optie uitgeschakeld om inloggen/aanmaken niet te blokkeren.", + "AWAIT_RESPONSE": "Wachten op reactie", + "AWAIT_RESPONSE_DESCRIPTION": "We wachten op een reactie voordat we iets anders doen. Handig als u van plan bent om meerdere doelen voor één actie te gebruiken" + }, + "TABLE": { + "NAME": "Naam", + "ENDPOINT": "Eindpunt", + "CREATIONDATE": "Aanmaakdatum" + } + } + }, "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", @@ -1356,6 +1490,7 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Privacybeleid", "OIDC": "OIDC Token levensduur en vervaldatum", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Secret Generator", "SECURITY": "Beveiligingsinstellingen", "EVENTS": "Evenementen", @@ -1515,7 +1650,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": { diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index c76236e9a6..b071c4f99a 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -185,6 +185,32 @@ "DESCRIPTION": "Czas życia bezczynnego tokena odświeżania to maksymalny czas, przez który token odświeżania może pozostać nieużywany." } }, + "WEB_KEYS": { + "DESCRIPTION": "Zarządzaj swoimi kluczami internetowymi OIDC, aby bezpiecznie podpisywać i weryfikować tokeny w swojej instancji ZITADEL.", + "TABLE": { + "TITLE": "Aktywne i Przyszłe Klucze Internetowe", + "DESCRIPTION": "Twoje aktywne i nadchodzące klucze internetowe. Aktywacja nowego klucza spowoduje dezaktywację obecnego.", + "NOTE": "Uwaga: Punkt końcowy JWKs OIDC zwraca odpowiedź możliwą do buforowania (domyślnie 5 minut). Unikaj zbyt wczesnej aktywacji klucza, ponieważ może on nie być jeszcze dostępny w pamięci podręcznej i dla klientów.", + "ACTIVATE": "Aktywuj następny klucz internetowy", + "ACTIVE": "Obecnie aktywny", + "NEXT": "Następny w kolejce", + "FUTURE": "Przyszłe", + "WARNING": "Klucz sieciowy ma mniej niż 5 minut" + }, + "CREATE": { + "TITLE": "Utwórz nowy klucz internetowy", + "DESCRIPTION": "Utworzenie nowego klucza internetowego doda go do Twojej listy. ZITADEL domyślnie używa kluczy RSA2048 z haszowaniem SHA256.", + "KEY_TYPE": "Typ klucza", + "BITS": "Bity", + "HASHER": "Haszowanie", + "CURVE": "Krzywa" + }, + "PREVIOUS_TABLE": { + "TITLE": "Poprzednie Klucze Internetowe", + "DESCRIPTION": "To są Twoje poprzednie klucze internetowe, które nie są już aktywne.", + "DEACTIVATED_ON": "Dezaktywowany dnia" + } + }, "MESSAGE_TEXTS": { "TITLE": "Teksty wiadomości", "DESCRIPTION": "Dostosuj teksty swoich e-maili lub wiadomości SMS z powiadomieniami. Jeśli chcesz wyłączyć niektóre języki, ogranicz je w ustawieniach językowych swoich instancji.", @@ -501,6 +527,114 @@ "DOWNLOAD": "Pobierz", "APPLY": "Stosować" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Akcje", + "DESCRIPTION": "Akcje umożliwiają uruchamianie niestandardowego kodu w odpowiedzi na żądania API, zdarzenia lub określone funkcje. Użyj ich, aby rozszerzyć Zitadel, zautomatyzować przepływy pracy i zintegrować się z innymi systemami.", + "TYPES": { + "request": "Żądanie", + "response": "Odpowiedź", + "events": "Zdarzenia", + "function": "Funkcja" + }, + "DIALOG": { + "CREATE_TITLE": "Utwórz akcję", + "UPDATE_TITLE": "Aktualizuj akcję", + "TYPE": { + "DESCRIPTION": "Wybierz, kiedy chcesz uruchomić tę akcję", + "REQUEST": { + "TITLE": "Żądanie", + "DESCRIPTION": "Żądania występujące w Zitadel. Może to być coś takiego jak wywołanie żądania logowania." + }, + "RESPONSE": { + "TITLE": "Odpowiedź", + "DESCRIPTION": "Odpowiedź na żądanie w Zitadel. Pomyśl o odpowiedzi, którą otrzymujesz po pobraniu użytkownika." + }, + "EVENTS": { + "TITLE": "Zdarzenia", + "DESCRIPTION": "Zdarzenia, które mają miejsce w Zitadel. Mogą to być dowolne zdarzenia, takie jak utworzenie konta użytkownika, udane logowanie itp." + }, + "FUNCTIONS": { + "TITLE": "Funkcje", + "DESCRIPTION": "Funkcje, które można wywołać w Zitadel. Mogą to być dowolne funkcje, od wysłania wiadomości e-mail po utworzenie użytkownika." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Wybierz, czy ta akcja ma zastosowanie do wszystkich żądań, określonej usługi (np. zarządzanie użytkownikami) czy pojedynczego żądania (np. utwórz użytkownika).", + "ALL": { + "TITLE": "Wszystkie", + "DESCRIPTION": "Wybierz tę opcję, jeśli chcesz uruchomić akcję dla każdego żądania" + }, + "SELECT_SERVICE": { + "TITLE": "Wybierz usługę", + "DESCRIPTION": "Wybierz usługę Zitadel dla swojej akcji." + }, + "SELECT_METHOD": { + "TITLE": "Wybierz metodę", + "DESCRIPTION": "Jeśli chcesz uruchomić tylko dla określonego żądania, wybierz je tutaj", + "NOTE": "Jeśli nie wybierzesz metody, akcja zostanie uruchomiona dla każdego żądania w wybranej usłudze." + }, + "FUNCTIONNAME": { + "TITLE": "Nazwa funkcji", + "DESCRIPTION": "Wybierz funkcję, którą chcesz uruchomić" + }, + "SELECT_GROUP": { + "TITLE": "Ustaw grupę", + "DESCRIPTION": "Jeśli chcesz uruchomić tylko dla grupy zdarzeń, ustaw grupę tutaj" + }, + "SELECT_EVENT": { + "TITLE": "Wybierz zdarzenie", + "DESCRIPTION": "Jeśli chcesz uruchomić tylko dla określonego zdarzenia, określ je tutaj" + } + }, + "TARGET": { + "DESCRIPTION": "Możesz wybrać uruchomienie celu lub uruchomienie go na tych samych warunkach co inne cele.", + "TARGET": { + "DESCRIPTION": "Cel, który chcesz uruchomić dla tej akcji" + }, + "CONDITIONS": { + "DESCRIPTION": "Warunki wykonania" + } + } + }, + "TABLE": { + "CONDITION": "Warunek", + "TYPE": "Typ", + "TARGET": "Cel", + "CREATIONDATE": "Data utworzenia" + } + }, + "TARGET": { + "TITLE": "Cele", + "DESCRIPTION": "Celem jest miejsce docelowe kodu, który chcesz uruchomić z akcji. Utwórz cel tutaj i dodaj go do swoich akcji.", + "CREATE": { + "TITLE": "Utwórz swój cel", + "DESCRIPTION": "Utwórz własny cel poza Zitadel", + "NAME": "Nazwa", + "NAME_DESCRIPTION": "Nadaj swojemu celowi jasną, opisową nazwę, aby ułatwić jego późniejszą identyfikację", + "TYPE": "Typ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Wywołanie REST", + "restAsync": "REST Asynchroniczny" + }, + "ENDPOINT": "Punkt końcowy", + "ENDPOINT_DESCRIPTION": "Wprowadź punkt końcowy, w którym hostowany jest Twój kod. Upewnij się, że jest dla nas dostępny!", + "TIMEOUT": "Limit czasu", + "TIMEOUT_DESCRIPTION": "Ustaw maksymalny czas, w jakim cel musi odpowiedzieć. Jeśli zajmie to więcej czasu, zatrzymamy żądanie.", + "INTERRUPT_ON_ERROR": "Przerwij w przypadku błędu", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Zatrzymaj wszystkie wykonania, gdy cele zwrócą błąd", + "INTERRUPT_ON_ERROR_WARNING": "Uwaga: „Przerwij w przypadku błędu” zatrzymuje operacje w przypadku błędu, co grozi zablokowaniem. Przetestuj przy wyłączonej opcji, aby uniknąć blokowania logowania/tworzenia.", + "AWAIT_RESPONSE": "Oczekuj na odpowiedź", + "AWAIT_RESPONSE_DESCRIPTION": "Przed wykonaniem jakichkolwiek innych czynności poczekamy na odpowiedź. Przydatne, jeśli zamierzasz użyć wielu celów dla jednej akcji" + }, + "TABLE": { + "NAME": "Nazwa", + "ENDPOINT": "Punkt końcowy", + "CREATIONDATE": "Data utworzenia" + } + } + }, "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", @@ -1355,6 +1489,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", @@ -1514,7 +1649,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": { diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index 427ce218e8..a8ab833724 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -185,6 +185,32 @@ "DESCRIPTION": "A vida útil do token de atualização inativo é o tempo máximo que um token de atualização pode ficar sem uso." } }, + "WEB_KEYS": { + "DESCRIPTION": "Gerencie suas Chaves Web OIDC para assinar e validar tokens com segurança em sua instância do ZITADEL.", + "TABLE": { + "TITLE": "Chaves Web Ativas e Futuras", + "DESCRIPTION": "Suas chaves web ativas e futuras. Ativar uma nova chave desativará a atual.", + "NOTE": "Nota: O endpoint JWKs OIDC retorna uma resposta que pode ser armazenada em cache (padrão: 5 min). Evite ativar uma chave muito cedo, pois ela pode ainda não estar disponível no cache e para os clientes.", + "ACTIVATE": "Ativar próxima Chave Web", + "ACTIVE": "Atualmente ativa", + "NEXT": "Próxima na fila", + "FUTURE": "Futuro", + "WARNING": "A chave da Web tem menos de 5 minutos" + }, + "CREATE": { + "TITLE": "Criar nova Chave Web", + "DESCRIPTION": "Criar uma nova chave web a adicionará à sua lista. O ZITADEL usa, por padrão, chaves RSA2048 com um algoritmo de hash SHA256.", + "KEY_TYPE": "Tipo de Chave", + "BITS": "Bits", + "HASHER": "Algoritmo de Hash", + "CURVE": "Curva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Chaves Web Anteriores", + "DESCRIPTION": "Estas são suas chaves web anteriores que não estão mais ativas.", + "DEACTIVATED_ON": "Desativada em" + } + }, "MESSAGE_TEXTS": { "TITLE": "Textos de Mensagens", "DESCRIPTION": "Personalize os textos do seu e-mail de notificação ou mensagens SMS. Se desejar desativar alguns idiomas, restrinja-os nas configurações de idioma da sua instância.", @@ -502,6 +528,114 @@ "DOWNLOAD": "Baixar", "APPLY": "Aplicar" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Ações", + "DESCRIPTION": "As ações permitem que você execute código personalizado em resposta a solicitações de API, eventos ou funções específicas. Use-as para estender o Zitadel, automatizar fluxos de trabalho e integrar-se a outros sistemas.", + "TYPES": { + "request": "Solicitação", + "response": "Resposta", + "events": "Eventos", + "function": "Função" + }, + "DIALOG": { + "CREATE_TITLE": "Criar uma Ação", + "UPDATE_TITLE": "Atualizar uma Ação", + "TYPE": { + "DESCRIPTION": "Selecione quando você deseja que esta Ação seja executada", + "REQUEST": { + "TITLE": "Solicitação", + "DESCRIPTION": "Solicitações que ocorrem dentro do Zitadel. Isso pode ser algo como uma chamada de solicitação de login." + }, + "RESPONSE": { + "TITLE": "Resposta", + "DESCRIPTION": "Uma resposta de uma solicitação dentro do Zitadel. Pense na resposta que você recebe ao buscar um usuário." + }, + "EVENTS": { + "TITLE": "Eventos", + "DESCRIPTION": "Eventos que acontecem dentro do Zitadel. Isso pode ser qualquer coisa, como um usuário criando uma conta, um login bem-sucedido, etc." + }, + "FUNCTIONS": { + "TITLE": "Funções", + "DESCRIPTION": "Funções que você pode chamar dentro do Zitadel. Isso pode ser qualquer coisa, desde enviar um e-mail até criar um usuário." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Escolha se esta ação se aplica a todas as solicitações, um serviço específico (por exemplo, gerenciamento de usuários) ou uma única solicitação (por exemplo, criar usuário).", + "ALL": { + "TITLE": "Todas", + "DESCRIPTION": "Selecione isso se você quiser executar sua ação em cada solicitação" + }, + "SELECT_SERVICE": { + "TITLE": "Selecionar Serviço", + "DESCRIPTION": "Escolha um Serviço Zitadel para sua ação." + }, + "SELECT_METHOD": { + "TITLE": "Selecionar Método", + "DESCRIPTION": "Se você quiser executar apenas em uma solicitação específica, selecione-a aqui", + "NOTE": "Se você não selecionar um método, sua ação será executada em todas as solicitações em seu serviço selecionado." + }, + "FUNCTIONNAME": { + "TITLE": "Nome da Função", + "DESCRIPTION": "Escolha a função que você deseja executar" + }, + "SELECT_GROUP": { + "TITLE": "Definir Grupo", + "DESCRIPTION": "Se você quiser executar apenas em um grupo de eventos, defina o grupo aqui" + }, + "SELECT_EVENT": { + "TITLE": "Selecionar Evento", + "DESCRIPTION": "Se você quiser executar apenas em um evento específico, especifique-o aqui" + } + }, + "TARGET": { + "DESCRIPTION": "Você pode escolher executar um destino ou executá-lo nas mesmas condições que outros destinos.", + "TARGET": { + "DESCRIPTION": "O destino que você deseja executar para esta ação" + }, + "CONDITIONS": { + "DESCRIPTION": "Condições de Execução" + } + } + }, + "TABLE": { + "CONDITION": "Condição", + "TYPE": "Tipo", + "TARGET": "Destino", + "CREATIONDATE": "Data de Criação" + } + }, + "TARGET": { + "TITLE": "Destinos", + "DESCRIPTION": "Um destino é o destino do código que você deseja executar a partir de uma ação. Crie um destino aqui e adicione-o às suas ações.", + "CREATE": { + "TITLE": "Criar seu Destino", + "DESCRIPTION": "Crie seu próprio destino fora do Zitadel", + "NAME": "Nome", + "NAME_DESCRIPTION": "Dê ao seu destino um nome claro e descritivo para torná-lo fácil de identificar mais tarde", + "TYPE": "Tipo", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Chamada REST", + "restAsync": "REST Assíncrono" + }, + "ENDPOINT": "Ponto de Extremidade", + "ENDPOINT_DESCRIPTION": "Insira o ponto de extremidade onde seu código está hospedado. Certifique-se de que ele esteja acessível para nós!", + "TIMEOUT": "Tempo Limite", + "TIMEOUT_DESCRIPTION": "Defina o tempo máximo que seu destino tem para responder. Se demorar mais, interromperemos a solicitação.", + "INTERRUPT_ON_ERROR": "Interromper em Caso de Erro", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Pare todas as execuções quando os destinos retornarem um erro", + "INTERRUPT_ON_ERROR_WARNING": "Atenção: “Interromper em caso de erro” interrompe as operações em caso de falha, com risco de bloqueio. Teste com esta opção desativada para evitar bloquear o login/criação.", + "AWAIT_RESPONSE": "Aguardar Resposta", + "AWAIT_RESPONSE_DESCRIPTION": "Aguardaremos uma resposta antes de fazermos qualquer outra coisa. Útil se você pretende usar vários destinos para uma única ação" + }, + "TABLE": { + "NAME": "Nome", + "ENDPOINT": "Ponto de Extremidade", + "CREATIONDATE": "Data de Criação" + } + } + }, "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", @@ -1357,6 +1491,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", @@ -1516,7 +1651,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": { diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json index 7e8fdda7dd..987368d84f 100644 --- a/console/src/assets/i18n/ro.json +++ b/console/src/assets/i18n/ro.json @@ -185,6 +185,32 @@ "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.", @@ -502,6 +528,114 @@ "DOWNLOAD": "Descărcați", "APPLY": "Aplicați" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Acțiuni", + "DESCRIPTION": "Acțiunile vă permit să rulați cod personalizat ca răspuns la cereri API, evenimente sau funcții specifice. Folosiți-le pentru a extinde Zitadel, a automatiza fluxurile de lucru și a vă integra cu alte sisteme.", + "TYPES": { + "request": "Cerere", + "response": "Răspuns", + "events": "Evenimente", + "function": "Funcție" + }, + "DIALOG": { + "CREATE_TITLE": "Creează o Acțiune", + "UPDATE_TITLE": "Actualizează o Acțiune", + "TYPE": { + "DESCRIPTION": "Selectați când doriți să rulați această Acțiune", + "REQUEST": { + "TITLE": "Cerere", + "DESCRIPTION": "Cereri care apar în Zitadel. Acesta ar putea fi ceva de genul unui apel de cerere de autentificare." + }, + "RESPONSE": { + "TITLE": "Răspuns", + "DESCRIPTION": "Un răspuns la o cerere în Zitadel. Gândiți-vă la răspunsul pe care îl primiți la preluarea unui utilizator." + }, + "EVENTS": { + "TITLE": "Evenimente", + "DESCRIPTION": "Evenimente care se întâmplă în Zitadel. Acesta ar putea fi orice, cum ar fi un utilizator care își creează un cont, o autentificare reușită etc." + }, + "FUNCTIONS": { + "TITLE": "Funcții", + "DESCRIPTION": "Funcții pe care le puteți apela în Zitadel. Acesta ar putea fi orice, de la trimiterea unui e-mail la crearea unui utilizator." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Alegeți dacă această acțiune se aplică tuturor cererilor, unui serviciu specific (de exemplu, gestionarea utilizatorilor) sau unei singure cereri (de exemplu, crearea unui utilizator).", + "ALL": { + "TITLE": "Toate", + "DESCRIPTION": "Selectați aceasta dacă doriți să rulați acțiunea la fiecare cerere" + }, + "SELECT_SERVICE": { + "TITLE": "Selectați Serviciul", + "DESCRIPTION": "Alegeți un Serviciu Zitadel pentru acțiunea dvs." + }, + "SELECT_METHOD": { + "TITLE": "Selectați Metoda", + "DESCRIPTION": "Dacă doriți să rulați numai la o cerere specifică, selectați-o aici", + "NOTE": "Dacă nu selectați o metodă, acțiunea dvs. va rula la fiecare cerere din serviciul selectat." + }, + "FUNCTIONNAME": { + "TITLE": "Numele Funcției", + "DESCRIPTION": "Alegeți funcția pe care doriți să o rulați" + }, + "SELECT_GROUP": { + "TITLE": "Setează Grupul", + "DESCRIPTION": "Dacă doriți să rulați numai pe un grup de evenimente, setați grupul aici" + }, + "SELECT_EVENT": { + "TITLE": "Selectați Evenimentul", + "DESCRIPTION": "Dacă doriți să rulați numai la un eveniment specific, specificați-l aici" + } + }, + "TARGET": { + "DESCRIPTION": "Puteți alege să rulați o țintă sau să o rulați în aceleași condiții ca și alte ținte.", + "TARGET": { + "DESCRIPTION": "Ținta pe care doriți să o rulați pentru această acțiune" + }, + "CONDITIONS": { + "DESCRIPTION": "Condiții de Execuție" + } + } + }, + "TABLE": { + "CONDITION": "Condiție", + "TYPE": "Tip", + "TARGET": "Țintă", + "CREATIONDATE": "Data Creării" + } + }, + "TARGET": { + "TITLE": "Ținte", + "DESCRIPTION": "O țintă este destinația codului pe care doriți să-l rulați dintr-o acțiune. Creați o țintă aici și adăugați-o la acțiunile dvs.", + "CREATE": { + "TITLE": "Creează-ți Ținta", + "DESCRIPTION": "Creați-vă propria țintă în afara Zitadel", + "NAME": "Nume", + "NAME_DESCRIPTION": "Dați țintei dvs. un nume clar, descriptiv, pentru a o identifica ușor mai târziu", + "TYPE": "Tip", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Apel REST", + "restAsync": "REST Asincron" + }, + "ENDPOINT": "Punct Final", + "ENDPOINT_DESCRIPTION": "Introduceți punctul final unde este găzduit codul dvs. Asigurați-vă că este accesibil pentru noi!", + "TIMEOUT": "Timeout", + "TIMEOUT_DESCRIPTION": "Setați timpul maxim pe care ținta dvs. îl are pentru a răspunde. Dacă durează mai mult, vom opri cererea.", + "INTERRUPT_ON_ERROR": "Întrerupe la Eroare", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Opriți toate execuțiile când țintele returnează o eroare", + "INTERRUPT_ON_ERROR_WARNING": "Atenție: „Întrerupe la eroare” oprește operațiunile în caz de eșec, riscând blocarea accesului. Testați cu această opțiune dezactivată pentru a evita blocarea autentificării/creării.", + "AWAIT_RESPONSE": "Așteaptă Răspuns", + "AWAIT_RESPONSE_DESCRIPTION": "Vom aștepta un răspuns înainte de a face altceva. Util dacă intenționați să utilizați mai multe ținte pentru o singură acțiune" + }, + "TABLE": { + "NAME": "Nume", + "ENDPOINT": "Punct Final", + "CREATIONDATE": "Data Creării" + } + } + }, "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", @@ -1354,6 +1488,7 @@ "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", @@ -1513,7 +1648,10 @@ }, "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ă." + "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": { diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index f3ebf21e7b..1289fb708f 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -185,6 +185,33 @@ "DESCRIPTION": "Срок действия неактивного токена обновления - это максимальное время, в течение которого токен обновления может оставаться неиспользованным." } }, + "WEB_KEYS": { + "DESCRIPTION": "Управляйте своими OIDC веб-ключами для безопасной подписи и валидации токенов в вашем экземпляре ZITADEL.", + "TABLE": { + "TITLE": "Активные и будущие веб-ключи", + "DESCRIPTION": "Ваши активные и будущие веб-ключи. Активация нового ключа приведёт к деактивации текущего.", + "NOTE": "Примечание: Конечная точка JWKs OIDC возвращает кэшируемый ответ (по умолчанию 5 минут). Избегайте слишком ранней активации ключа, так как он может ещё не быть доступен в кэше и у клиентов.", + "ACTIVATE": "Активировать следующий веб-ключ", + "ACTIVE": "В настоящее время активен", + "NEXT": "Следующий в очереди", + "FUTURE": "Будущий", + "WARNING": "Веб-ключу менее 5 минут" + }, + "CREATE": { + "TITLE": "Создать новый веб-ключ", + "DESCRIPTION": "Создание нового веб-ключа добавит его в ваш список. ZITADEL по умолчанию использует ключи RSA2048 с хешированием SHA256.", + "KEY_TYPE": "Тип ключа", + "BITS": "Биты", + "HASHER": "Алгоритм хеширования", + "CURVE": "Кривая" + }, + "PREVIOUS_TABLE": { + "TITLE": "Предыдущие веб-ключи", + "DESCRIPTION": "Это ваши предыдущие веб-ключи, которые больше не активны.", + "DEACTIVATED_ON": "Деактивирован" + } + }, + "MESSAGE_TEXTS": { "TITLE": "Тексты сообщений", "DESCRIPTION": "Настройте тексты ваших уведомлений по электронной почте или SMS. Если вы хотите отключить некоторые языки, ограничьте их в настройках языка ваших экземпляров.", @@ -501,6 +528,114 @@ "DOWNLOAD": "Скачать", "APPLY": "Применять" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Действия", + "DESCRIPTION": "Действия позволяют запускать пользовательский код в ответ на API-запросы, события или определенные функции. Используйте их для расширения Zitadel, автоматизации рабочих процессов и интеграции с другими системами.", + "TYPES": { + "request": "Запрос", + "response": "Ответ", + "events": "События", + "function": "Функция" + }, + "DIALOG": { + "CREATE_TITLE": "Создать действие", + "UPDATE_TITLE": "Обновить действие", + "TYPE": { + "DESCRIPTION": "Выберите, когда вы хотите запустить это действие", + "REQUEST": { + "TITLE": "Запрос", + "DESCRIPTION": "Запросы, которые происходят внутри Zitadel. Это может быть что-то вроде вызова запроса на вход." + }, + "RESPONSE": { + "TITLE": "Ответ", + "DESCRIPTION": "Ответ на запрос внутри Zitadel. Подумайте об ответе, который вы получаете при получении пользователя." + }, + "EVENTS": { + "TITLE": "События", + "DESCRIPTION": "События, которые происходят внутри Zitadel. Это может быть что угодно, например, создание пользователем учетной записи, успешный вход и т. д." + }, + "FUNCTIONS": { + "TITLE": "Функции", + "DESCRIPTION": "Функции, которые вы можете вызвать внутри Zitadel. Это может быть что угодно, от отправки электронной почты до создания пользователя." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Выберите, применяется ли это действие ко всем запросам, к определенной службе (например, управление пользователями) или к одному запросу (например, создать пользователя).", + "ALL": { + "TITLE": "Все", + "DESCRIPTION": "Выберите это, если вы хотите запустить свое действие при каждом запросе" + }, + "SELECT_SERVICE": { + "TITLE": "Выбрать службу", + "DESCRIPTION": "Выберите службу Zitadel для вашего действия." + }, + "SELECT_METHOD": { + "TITLE": "Выбрать метод", + "DESCRIPTION": "Если вы хотите запустить только для определенного запроса, выберите его здесь", + "NOTE": "Если вы не выберете метод, ваше действие будет запускаться при каждом запросе в выбранной вами службе." + }, + "FUNCTIONNAME": { + "TITLE": "Имя функции", + "DESCRIPTION": "Выберите функцию, которую вы хотите запустить" + }, + "SELECT_GROUP": { + "TITLE": "Установить группу", + "DESCRIPTION": "Если вы хотите запустить только для группы событий, установите группу здесь" + }, + "SELECT_EVENT": { + "TITLE": "Выбрать событие", + "DESCRIPTION": "Если вы хотите запустить только для определенного события, укажите его здесь" + } + }, + "TARGET": { + "DESCRIPTION": "Вы можете выбрать запуск цели или запустить ее в тех же условиях, что и другие цели.", + "TARGET": { + "DESCRIPTION": "Цель, которую вы хотите запустить для этого действия" + }, + "CONDITIONS": { + "DESCRIPTION": "Условия выполнения" + } + } + }, + "TABLE": { + "CONDITION": "Условие", + "TYPE": "Тип", + "TARGET": "Цель", + "CREATIONDATE": "Дата создания" + } + }, + "TARGET": { + "TITLE": "Цели", + "DESCRIPTION": "Цель — это место назначения кода, который вы хотите запустить из действия. Создайте цель здесь и добавьте ее к своим действиям.", + "CREATE": { + "TITLE": "Создать свою цель", + "DESCRIPTION": "Создайте свою собственную цель за пределами Zitadel", + "NAME": "Имя", + "NAME_DESCRIPTION": "Дайте своей цели четкое, описательное имя, чтобы ее было легко идентифицировать позже", + "TYPE": "Тип", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Вызов", + "restAsync": "REST Асинхронный" + }, + "ENDPOINT": "Конечная точка", + "ENDPOINT_DESCRIPTION": "Введите конечную точку, где размещен ваш код. Убедитесь, что он доступен для нас!", + "TIMEOUT": "Тайм-аут", + "TIMEOUT_DESCRIPTION": "Установите максимальное время, в течение которого ваша цель должна ответить. Если это займет больше времени, мы остановим запрос.", + "INTERRUPT_ON_ERROR": "Прервать при ошибке", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Остановите все выполнения, когда цели вернут ошибку", + "INTERRUPT_ON_ERROR_WARNING": "Внимание: опция «Прервать при ошибке» останавливает выполнение при сбое, что может привести к блокировке. Протестируйте с отключённой опцией, чтобы избежать блокировки входа/создания.", + "AWAIT_RESPONSE": "Ожидать ответа", + "AWAIT_RESPONSE_DESCRIPTION": "Мы подождем ответа, прежде чем делать что-либо еще. Полезно, если вы планируете использовать несколько целей для одного действия" + }, + "TABLE": { + "NAME": "Имя", + "ENDPOINT": "Конечная точка", + "CREATIONDATE": "Дата создания" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Имеет контроль над всем экземпляром, включая все организации", "IAM_OWNER_VIEWER": "Имеет разрешение на просмотр всего экземпляра, включая все организации", @@ -1400,6 +1535,7 @@ "BRANDING": "Брендинг", "PRIVACYPOLICY": "Политика конфиденциальности", "OIDC": "Срок действия токена OIDC", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Отображение ключа", "SECURITY": "Настройки безопасности", "EVENTS": "События", @@ -1567,7 +1703,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": { diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index 03e6f89862..44ca2fc200 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -185,6 +185,32 @@ "DESCRIPTION": "Den inaktiva förnyelsetokenens livslängd är den maximala tiden en förnyelsetoken kan vara oanvänd." } }, + "WEB_KEYS": { + "DESCRIPTION": "Hantera dina OIDC-webbnycklar för att säkert signera och validera tokens för din ZITADEL-instans.", + "TABLE": { + "TITLE": "Aktiva och framtida webbnycklar", + "DESCRIPTION": "Dina aktiva och kommande webbnycklar. Aktivering av en ny nyckel kommer att inaktivera den nuvarande.", + "NOTE": "Observera: JWKs OIDC-slutpunkten returnerar ett cachebart svar (standard 5 min). Undvik att aktivera en nyckel för tidigt, eftersom den kanske ännu inte är tillgänglig i cache och för klienter.", + "ACTIVATE": "Aktivera nästa webbnyckel", + "ACTIVE": "För närvarande aktiv", + "NEXT": "Nästa i kön", + "FUTURE": "Framtida", + "WARNING": "Webbnyckeln är mindre än 5 minuter gammal" + }, + "CREATE": { + "TITLE": "Skapa ny webbnyckel", + "DESCRIPTION": "Att skapa en ny webbnyckel lägger till den i din lista. ZITADEL använder som standard RSA2048-nycklar med en SHA256-hasher.", + "KEY_TYPE": "Nyckeltyp", + "BITS": "Bitar", + "HASHER": "Hasher", + "CURVE": "Kurva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Tidigare webbnycklar", + "DESCRIPTION": "Detta är dina tidigare webbnycklar som inte längre är aktiva.", + "DEACTIVATED_ON": "Inaktiverad den" + } + }, "MESSAGE_TEXTS": { "TITLE": "Meddelandetexter", "DESCRIPTION": "Anpassa texterna i dina notifikationsmail eller SMS-meddelanden. Om du vill inaktivera några av språken, begränsa dem i dina instansers språkinställningar.", @@ -502,6 +528,114 @@ "DOWNLOAD": "Ladda ner", "APPLY": "Tillämpa" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Åtgärder", + "DESCRIPTION": "Åtgärder låter dig köra anpassad kod som svar på API-förfrågningar, händelser eller specifika funktioner. Använd dem för att utöka Zitadel, automatisera arbetsflöden och integrera med andra system.", + "TYPES": { + "request": "Förfrågan", + "response": "Svar", + "events": "Händelser", + "function": "Funktion" + }, + "DIALOG": { + "CREATE_TITLE": "Skapa en åtgärd", + "UPDATE_TITLE": "Uppdatera en åtgärd", + "TYPE": { + "DESCRIPTION": "Välj när du vill att denna åtgärd ska köras", + "REQUEST": { + "TITLE": "Förfrågan", + "DESCRIPTION": "Förfrågningar som sker inom Zitadel. Detta kan vara något som ett inloggningsförfrågningsanrop." + }, + "RESPONSE": { + "TITLE": "Svar", + "DESCRIPTION": "Ett svar från en förfrågan inom Zitadel. Tänk på svaret du får tillbaka från att hämta en användare." + }, + "EVENTS": { + "TITLE": "Händelser", + "DESCRIPTION": "Händelser som händer inom Zitadel. Detta kan vara vad som helst som en användare som skapar ett konto, en lyckad inloggning etc." + }, + "FUNCTIONS": { + "TITLE": "Funktioner", + "DESCRIPTION": "Funktioner som du kan anropa inom Zitadel. Detta kan vara allt från att skicka ett e-postmeddelande till att skapa en användare." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Välj om denna åtgärd gäller för alla förfrågningar, en specifik tjänst (t.ex. användarhantering) eller en enskild förfrågan (t.ex. skapa användare).", + "ALL": { + "TITLE": "Alla", + "DESCRIPTION": "Välj detta om du vill köra din åtgärd på varje förfrågan" + }, + "SELECT_SERVICE": { + "TITLE": "Välj tjänst", + "DESCRIPTION": "Välj en Zitadel-tjänst för din åtgärd." + }, + "SELECT_METHOD": { + "TITLE": "Välj metod", + "DESCRIPTION": "Om du bara vill köra på en specifik förfrågan, välj den här", + "NOTE": "Om du inte väljer en metod körs din åtgärd på varje förfrågan i din valda tjänst." + }, + "FUNCTIONNAME": { + "TITLE": "Funktionsnamn", + "DESCRIPTION": "Välj den funktion du vill köra" + }, + "SELECT_GROUP": { + "TITLE": "Ange grupp", + "DESCRIPTION": "Om du bara vill köra på en grupp händelser, ange gruppen här" + }, + "SELECT_EVENT": { + "TITLE": "Välj händelse", + "DESCRIPTION": "Om du bara vill köra på en specifik händelse, ange den här" + } + }, + "TARGET": { + "DESCRIPTION": "Du kan välja att köra ett mål eller att köra det under samma villkor som andra mål.", + "TARGET": { + "DESCRIPTION": "Målet du vill köra för denna åtgärd" + }, + "CONDITIONS": { + "DESCRIPTION": "Körningsvillkor" + } + } + }, + "TABLE": { + "CONDITION": "Villkor", + "TYPE": "Typ", + "TARGET": "Mål", + "CREATIONDATE": "Skapat datum" + } + }, + "TARGET": { + "TITLE": "Mål", + "DESCRIPTION": "Ett mål är destinationen för koden du vill köra från en åtgärd. Skapa ett mål här och lägg till det i dina åtgärder.", + "CREATE": { + "TITLE": "Skapa ditt mål", + "DESCRIPTION": "Skapa ditt eget mål utanför Zitadel", + "NAME": "Namn", + "NAME_DESCRIPTION": "Ge ditt mål ett tydligt, beskrivande namn för att göra det enkelt att identifiera senare", + "TYPE": "Typ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Anrop", + "restAsync": "REST Asynkron" + }, + "ENDPOINT": "Slutpunkt", + "ENDPOINT_DESCRIPTION": "Ange slutpunkten där din kod finns. Se till att den är tillgänglig för oss!", + "TIMEOUT": "Tidsgräns", + "TIMEOUT_DESCRIPTION": "Ange den maximala tid ditt mål har att svara. Om det tar längre tid stoppar vi förfrågan.", + "INTERRUPT_ON_ERROR": "Avbryt vid fel", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Stoppa alla körningar när målen returnerar ett fel", + "INTERRUPT_ON_ERROR_WARNING": "Varning: ”Avbryt vid fel” stoppar åtgärder vid fel och kan leda till att du blir utelåst. Testa med funktionen avstängd för att undvika att blockera inloggning/skapa.", + "AWAIT_RESPONSE": "Vänta på svar", + "AWAIT_RESPONSE_DESCRIPTION": "Vi väntar på ett svar innan vi gör något annat. Användbart om du avser att använda flera mål för en enda åtgärd" + }, + "TABLE": { + "NAME": "Namn", + "ENDPOINT": "Slutpunkt", + "CREATIONDATE": "Skapat datum" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Har kontroll över hela instansen, inklusive alla organisationer", "IAM_OWNER_VIEWER": "Har behörighet att granska hela instansen, inklusive alla organisationer", @@ -1360,6 +1494,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", @@ -1519,7 +1654,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": { diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 4346bb2175..5df3b414ab 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -185,6 +185,32 @@ "DESCRIPTION": "空闲刷新令牌的生命周期是刷新令牌可以未使用的最长时间。" } }, + "WEB_KEYS": { + "DESCRIPTION": "管理您的 OIDC Web 密钥,以安全地签署和验证您的 ZITADEL 实例的令牌。", + "TABLE": { + "TITLE": "活动和未来的 Web 密钥", + "DESCRIPTION": "您的当前活动和即将启用的 Web 密钥。激活新密钥将会停用当前密钥。", + "NOTE": "注意:JWKs OIDC 端点返回可缓存的响应(默认 5 分钟)。请避免过早激活密钥,否则它可能尚未在缓存或客户端中可用。", + "ACTIVATE": "激活下一个 Web 密钥", + "ACTIVE": "当前活动", + "NEXT": "队列中的下一个", + "FUTURE": "未来", + "WARNING": "Web密钥不到5分钟。" + }, + "CREATE": { + "TITLE": "创建新的 Web 密钥", + "DESCRIPTION": "创建新的 Web 密钥会将其添加到您的列表。ZITADEL 默认使用 RSA2048 密钥和 SHA256 哈希算法。", + "KEY_TYPE": "密钥类型", + "BITS": "位数", + "HASHER": "哈希算法", + "CURVE": "曲线" + }, + "PREVIOUS_TABLE": { + "TITLE": "先前的 Web 密钥", + "DESCRIPTION": "这些是您之前使用但不再活动的 Web 密钥。", + "DEACTIVATED_ON": "停用时间" + } + }, "MESSAGE_TEXTS": { "TITLE": "消息文本", "DESCRIPTION": "自定义您的通知电子邮件或短信消息的文本。如果您想禁用某些语言,请在您的实例语言设置中限制它们。", @@ -502,6 +528,114 @@ "DOWNLOAD": "下载", "APPLY": "申请" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "操作", + "DESCRIPTION": "操作允许您运行自定义代码以响应 API 请求、事件或特定函数。使用它们来扩展 Zitadel、自动化工作流程并与其他系统集成。", + "TYPES": { + "request": "请求", + "response": "响应", + "events": "事件", + "function": "函数" + }, + "DIALOG": { + "CREATE_TITLE": "创建操作", + "UPDATE_TITLE": "更新操作", + "TYPE": { + "DESCRIPTION": "选择您希望此操作运行的时间", + "REQUEST": { + "TITLE": "请求", + "DESCRIPTION": "Zitadel 中发生的请求。这可能是登录请求调用之类的操作。" + }, + "RESPONSE": { + "TITLE": "响应", + "DESCRIPTION": "来自 Zitadel 中请求的响应。考虑一下从获取用户返回的响应。" + }, + "EVENTS": { + "TITLE": "事件", + "DESCRIPTION": "Zitadel 中发生的事件。这可能是用户创建帐户、成功登录等任何操作。" + }, + "FUNCTIONS": { + "TITLE": "函数", + "DESCRIPTION": "您可以在 Zitadel 中调用的函数。这可能是从发送电子邮件到创建用户的任何操作。" + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "选择此操作是适用于所有请求、特定服务(例如用户管理)还是单个请求(例如创建用户)。", + "ALL": { + "TITLE": "全部", + "DESCRIPTION": "如果您希望在每个请求上运行您的操作,请选择此项" + }, + "SELECT_SERVICE": { + "TITLE": "选择服务", + "DESCRIPTION": "为您的操作选择一个 Zitadel 服务。" + }, + "SELECT_METHOD": { + "TITLE": "选择方法", + "DESCRIPTION": "如果您只想在特定请求上执行,请在此处选择它", + "NOTE": "如果您不选择方法,您的操作将在您选择的服务中的每个请求上运行。" + }, + "FUNCTIONNAME": { + "TITLE": "函数名称", + "DESCRIPTION": "选择您要执行的函数" + }, + "SELECT_GROUP": { + "TITLE": "设置组", + "DESCRIPTION": "如果您只想在事件组上执行,请在此处设置组" + }, + "SELECT_EVENT": { + "TITLE": "选择事件", + "DESCRIPTION": "如果您只想在特定事件上执行,请在此处指定它" + } + }, + "TARGET": { + "DESCRIPTION": "您可以选择执行目标,或在与其他目标相同的条件下运行它。", + "TARGET": { + "DESCRIPTION": "您要为此操作执行的目标" + }, + "CONDITIONS": { + "DESCRIPTION": "执行条件" + } + } + }, + "TABLE": { + "CONDITION": "条件", + "TYPE": "类型", + "TARGET": "目标", + "CREATIONDATE": "创建日期" + } + }, + "TARGET": { + "TITLE": "目标", + "DESCRIPTION": "目标是您要从操作中执行的代码的目标。在此处创建一个目标并将其添加到您的操作中。", + "CREATE": { + "TITLE": "创建您的目标", + "DESCRIPTION": "在 Zitadel 外部创建您自己的目标", + "NAME": "名称", + "NAME_DESCRIPTION": "为您的目标提供清晰、描述性的名称,以便稍后轻松识别", + "TYPE": "类型", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST 调用", + "restAsync": "REST 异步" + }, + "ENDPOINT": "端点", + "ENDPOINT_DESCRIPTION": "输入您的代码托管的端点。确保我们可以访问它!", + "TIMEOUT": "超时", + "TIMEOUT_DESCRIPTION": "设置您的目标必须响应的最大时间。如果花费的时间更长,我们将停止请求。", + "INTERRUPT_ON_ERROR": "错误时中断", + "INTERRUPT_ON_ERROR_DESCRIPTION": "当目标返回错误时,停止所有执行", + "INTERRUPT_ON_ERROR_WARNING": "注意:“出错时中断”会在失败时停止操作,存在被锁定的风险。请在禁用该选项的情况下进行测试,以避免阻止登录/创建。", + "AWAIT_RESPONSE": "等待响应", + "AWAIT_RESPONSE_DESCRIPTION": "我们将在执行任何其他操作之前等待响应。如果您打算为单个操作使用多个目标,这将非常有用" + }, + "TABLE": { + "NAME": "名称", + "ENDPOINT": "端点", + "CREATIONDATE": "创建日期" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "控制整个实例,包括所有组织", "IAM_OWNER_VIEWER": "有权审查整个实例,包括所有组织", @@ -1356,6 +1490,7 @@ "BRANDING": "品牌标识", "PRIVACYPOLICY": "隐私政策", "OIDC": "OIDC 令牌有效期和过期时间", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "验证码外观", "SECURITY": "安全设置", "EVENTS": "活动", @@ -1515,7 +1650,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": { 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..5dd602dfa8 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -3464,13 +3464,20 @@ "@zitadel/proto" "1.0.4" jose "^5.3.0" -"@zitadel/proto@1.0.4", "@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== dependencies: "@bufbuild/protobuf" "^2.2.2" +"@zitadel/proto@1.0.5-sha-4118a9d": + version "1.0.5-sha-4118a9d" + resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.5-sha-4118a9d.tgz#e09025f31b2992b061d5416a0d1e12ef370118cc" + integrity sha512-7ZFwISL7TqdCkfEUx7/H6UJDqX8ZP2jqG1ulbELvEQ2smrK365Zs7AkJGeB/xbVdhQW9BOhWy2R+Jni7sfxd2w== + dependencies: + "@bufbuild/protobuf" "^2.2.2" + "@zkochan/js-yaml@0.0.6": version "0.0.6" resolved "https://registry.yarnpkg.com/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz#975f0b306e705e28b8068a07737fa46d3fc04826" diff --git a/docs/docs/apis/actions/v3/testing-locally.md b/docs/docs/apis/actions/v2/testing-locally.md similarity index 92% rename from docs/docs/apis/actions/v3/testing-locally.md rename to docs/docs/apis/actions/v2/testing-locally.md index b5b3cb389f..9ded5a5db4 100644 --- a/docs/docs/apis/actions/v3/testing-locally.md +++ b/docs/docs/apis/actions/v2/testing-locally.md @@ -66,10 +66,10 @@ Where you can replace 'signingKey' with the key received in the next step 'Creat 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) +[Create a target](/apis/resources/action_service_v2/action-service-create-target) ```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 ' \ @@ -89,10 +89,10 @@ Save the returned ID to set in the 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. -[Set an execution](/apis/resources/action_service_v3/zitadel-actions-set-execution) +[Set an execution](/apis/resources/action_service_v2/action-service-set-execution) ```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 ' \ diff --git a/docs/docs/apis/actions/v3/usage.md b/docs/docs/apis/actions/v2/usage.md similarity index 92% rename from docs/docs/apis/actions/v3/usage.md rename to docs/docs/apis/actions/v2/usage.md index 2e89f3ce36..aab1a6dd7b 100644 --- a/docs/docs/apis/actions/v3/usage.md +++ b/docs/docs/apis/actions/v2/usage.md @@ -62,14 +62,14 @@ There are different types of 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) +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_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). +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). ## Execution @@ -83,7 +83,7 @@ The condition can be defined for 4 types of processes: - `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) +The API documentation to set an Execution can be found [here](/apis/resources/action_service_v2/action-service-set-execution) ### Condition Best Match @@ -165,8 +165,8 @@ For Request and Response there are 3 levels the condition can be defined: - `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` +- [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 @@ -177,7 +177,7 @@ Replace the current Actions with the following flows: - [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). +The available conditions can be found under [all available Functions](/apis/resources/action_service_v2/action-service-list-execution-functions). ### Condition for Events 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/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 85f05b7c29..83c3ad480c 100644 --- a/docs/docs/concepts/features/actions_v2.md +++ b/docs/docs/concepts/features/actions_v2.md @@ -46,5 +46,5 @@ Currently, the defined Actions v2 will be executed additionally to the defined [ ## 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](/apis/actions/v2/usage) +- [Actions v2 example execution locally](/apis/actions/v2/testing-locally) \ No newline at end of file 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 e048eeb4e9..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 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/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/self-hosting/deploy/docker-compose-sa.yaml b/docs/docs/self-hosting/deploy/docker-compose-sa.yaml index 95608fd76d..9edd95faa0 100644 --- a/docs/docs/self-hosting/deploy/docker-compose-sa.yaml +++ b/docs/docs/self-hosting/deploy/docker-compose-sa.yaml @@ -32,7 +32,7 @@ services: db: restart: 'always' - image: postgres:16-alpine + image: postgres:17-alpine environment: PGUSER: postgres POSTGRES_PASSWORD: postgres diff --git a/docs/docs/self-hosting/deploy/docker-compose.yaml b/docs/docs/self-hosting/deploy/docker-compose.yaml index e32700ace4..f5164eb3b7 100644 --- a/docs/docs/self-hosting/deploy/docker-compose.yaml +++ b/docs/docs/self-hosting/deploy/docker-compose.yaml @@ -24,7 +24,7 @@ services: db: restart: 'always' - image: postgres:16-alpine + image: postgres:17-alpine environment: PGUSER: postgres POSTGRES_PASSWORD: postgres diff --git a/docs/docs/self-hosting/deploy/linux.mdx b/docs/docs/self-hosting/deploy/linux.mdx index eb7f4dc90d..90774e97ab 100644 --- a/docs/docs/self-hosting/deploy/linux.mdx +++ b/docs/docs/self-hosting/deploy/linux.mdx @@ -1,5 +1,5 @@ --- -title: Install ZITADEL on Linux +title: Install Zitadel on Linux sidebar_label: Linux --- @@ -11,7 +11,7 @@ import NoteInstanceNotFound from "./troubleshooting/_note_instance_not_found.mdx ## Install PostgreSQL Download a `postgresql` binary as described [in the PostgreSQL docs](https://www.postgresql.org/download/linux/). -ZITADEL is tested against PostgreSQL and CockroachDB latest stable tag and Ubuntu 22.04. +Zitadel is tested against PostgreSQL latest stable tag and latest Ubuntu LTS. ## Run PostgreSQL @@ -20,15 +20,15 @@ sudo systemctl start postgresql sudo systemctl enable postgresql ``` -## Install ZITADEL +## Install Zitadel -Download the ZITADEL release according to your architecture from [Github](https://github.com/zitadel/zitadel/releases/latest), unpack the archive and copy zitadel binary to /usr/local/bin +Download the Zitadel release according to your architecture from [Github](https://github.com/zitadel/zitadel/releases/latest), unpack the archive and copy zitadel binary to /usr/local/bin ```bash LATEST=$(curl -i https://github.com/zitadel/zitadel/releases/latest | grep location: | cut -d '/' -f 8 | tr -d '\r'); ARCH=$(uname -m); case $ARCH in armv5*) ARCH="armv5";; armv6*) ARCH="armv6";; armv7*) ARCH="arm";; aarch64) ARCH="arm64";; x86) ARCH="386";; x86_64) ARCH="amd64";; i686) ARCH="386";; i386) ARCH="386";; esac; wget -c https://github.com/zitadel/zitadel/releases/download/$LATEST/zitadel-linux-$ARCH.tar.gz -O - | tar -xz && sudo mv zitadel-linux-$ARCH/zitadel /usr/local/bin ``` -## Run ZITADEL +## Run Zitadel ```bash ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=root ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=postgres ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable ZITADEL_EXTERNALSECURE=false zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled @@ -50,7 +50,7 @@ ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZIT allowfullscreen > -### Setup ZITADEL with a service account +### Setup Zitadel with a service account ```bash ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=root ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable ZITADEL_EXTERNALSECURE=false ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH=/tmp/zitadel-admin-sa.json ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_TYPE=1 zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml index 94d8f438dc..d1d8c95bb2 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml @@ -25,7 +25,7 @@ services: - './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro' db: - image: postgres:16-alpine + image: postgres:17-alpine restart: always environment: - POSTGRES_USER=root diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx index 7f25a9b210..d5e3984568 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx @@ -81,5 +81,5 @@ Read more about [the login process](/guides/integrate/login/oidc/login-users). ## Troubleshooting -You can connect to cockroach like this: `docker exec -it loadbalancing-example-my-cockroach-db-1 cockroach sql --host my-cockroach-db --certs-dir /cockroach/certs/` -For example, to show all login names: `docker exec -it loadbalancing-example-my-cockroach-db-1 cockroach sql --database zitadel --host my-cockroach-db --certs-dir /cockroach/certs/ --execute "select * from projections.login_names3"` +You can connect to the database like this: `docker exec -it loadbalancing-example-db-1 psql --host localhost` +For example, to show all login names: `docker exec -it loadbalancing-example-db-1 psql -d zitadel --host localhost -c 'select * from projections.login_names3'` diff --git a/docs/docs/self-hosting/deploy/macos.mdx b/docs/docs/self-hosting/deploy/macos.mdx index f736255478..beb3182208 100644 --- a/docs/docs/self-hosting/deploy/macos.mdx +++ b/docs/docs/self-hosting/deploy/macos.mdx @@ -11,7 +11,7 @@ import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx ## Install PostgreSQL Download a `postgresql` binary as described [in the PostgreSQL docs](https://www.postgresql.org/download/macosx/). -ZITADEL is tested against PostgreSQL and CockroachDB latest stable tag and Ubuntu 22.04. +ZITADEL is tested against PostgreSQL latest stable tag and latest Ubuntu LTS. ## Run PostgreSQL diff --git a/docs/docs/self-hosting/deploy/overview.mdx b/docs/docs/self-hosting/deploy/overview.mdx index 38517c52f4..68255d4ce3 100644 --- a/docs/docs/self-hosting/deploy/overview.mdx +++ b/docs/docs/self-hosting/deploy/overview.mdx @@ -14,7 +14,7 @@ Choose your platform and run ZITADEL with the most minimal configuration possibl ## Prerequisites - For test environments, ZITADEL does not need many resources, 1 CPU and 512MB memory are more than enough. (With more CPU, the password hashing might be faster) -- A PostgreSQL or CockroachDB as only needed storage. Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide to use Postgresql. +- A PostgreSQL as only needed storage. Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide to use Postgresql. ## Releases diff --git a/docs/docs/self-hosting/manage/cache.md b/docs/docs/self-hosting/manage/cache.md index 30619ad283..32973d5586 100644 --- a/docs/docs/self-hosting/manage/cache.md +++ b/docs/docs/self-hosting/manage/cache.md @@ -110,7 +110,6 @@ Drawbacks: - Slowest of the available caching options - Might put additional strain on the database server, limiting horizontal scalability -- CockroachDB does not support unlogged tables. When this connector is enabled against CockroachDB, it does work but little to no performance benefit is to be expected. ### Local memory cache diff --git a/docs/docs/self-hosting/manage/cli/mirror.mdx b/docs/docs/self-hosting/manage/cli/mirror.mdx index 1c32dc8741..45bac9b279 100644 --- a/docs/docs/self-hosting/manage/cli/mirror.mdx +++ b/docs/docs/self-hosting/manage/cli/mirror.mdx @@ -1,5 +1,5 @@ --- -title: Mirror data to another database +title: Mirror data from cockroach to postgres sidebar_label: Mirror command --- @@ -9,15 +9,15 @@ The data can be mirrored to multiple database without influencing each other. ## Use cases -Migrate from cockroachdb to postgres or vice versa. +Migrate from cockroachdb to postgres. Replicate data to a secondary environment for testing. ## Prerequisites -You need an existing source database, most probably the database ZITADEL currently serves traffic from. +You need an existing source database, most probably the database Zitadel currently serves traffic from. -To mirror the data the destination database needs to be initialized and setup without an instance. +To mirror the data, the destination database needs to be initialized and set up without an instance. You can find the commands to start an empty Zitadel deployment in [the example section](#prepare-the-destination-database). ### Start the destination database @@ -38,14 +38,32 @@ docker compose up db --detach ## Example -The following commands setup the database as described above. See [configuration](#configuration) for more details about the configuration options. +### Prepare the destination database + +The following commands setup the database without an instance. ```bash zitadel init --config /path/to/your/new/config.yaml zitadel setup --for-mirror --config /path/to/your/new/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment +``` + +### Mirror the data + +The next step is to copy the data from the source to the destination database. For detailed configuration options, please refer to the [configuration section](#configuration). + +```bash zitadel mirror --system --config /path/to/your/mirror/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment ``` +### Initialize the data and verify + +The last step is to setup the permissions and verify the data, there might be differences between source and destination, refer to [`zitadel mirror verify`](#zitadel-mirror-verify) to get an overview of possible diffs. + +```bash +zitadel setup --for-mirror --config /path/to/your/new/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment +zitadel mirror verify --system --config /path/to/your/mirror/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment +``` + ## Usage The general syntax for the mirror command is: @@ -73,7 +91,7 @@ Flags: --masterkey string masterkey as argument for en/decryption keys -m, --masterkeyFile string path to the masterkey for en/decryption keys --masterkeyFromEnv read masterkey for en/decryption keys from environment variable (ZITADEL_MASTERKEY) - --tlsMode externalSecure start ZITADEL with (enabled), without (disabled) TLS or external component e.g. reverse proxy (external) terminating TLS, this flag will overwrite externalSecure and `tls.enabled` in configs files + --tlsMode externalSecure start Zitadel with (enabled), without (disabled) TLS or external component e.g. reverse proxy (external) terminating TLS, this flag will overwrite externalSecure and `tls.enabled` in configs files ``` ## Configuration @@ -87,8 +105,6 @@ Source: Database: zitadel # ZITADEL_SOURCE_COCKROACH_DATABASE MaxOpenConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXOPENCONNS MaxIdleConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXIDLECONNS - EventPushConnRatio: 0.33 # ZITADEL_SOURCE_COCKROACH_EVENTPUSHCONNRATIO - ProjectionSpoolerConnRatio: 0.33 # ZITADEL_SOURCE_COCKROACH_PROJECTIONSPOOLERCONNRATIO MaxConnLifetime: 30m # ZITADEL_SOURCE_COCKROACH_MAXCONNLIFETIME MaxConnIdleTime: 5m # ZITADEL_SOURCE_COCKROACH_MAXCONNIDLETIME Options: "" # ZITADEL_SOURCE_COCKROACH_OPTIONS @@ -122,44 +138,23 @@ Source: # The destination database the data are copied to. Use either cockroach or postgres, by default cockroach is used Destination: - cockroach: - Host: localhost # ZITADEL_DESTINATION_COCKROACH_HOST - Port: 26257 # ZITADEL_DESTINATION_COCKROACH_PORT - Database: zitadel # ZITADEL_DESTINATION_COCKROACH_DATABASE - MaxOpenConns: 0 # ZITADEL_DESTINATION_COCKROACH_MAXOPENCONNS - MaxIdleConns: 0 # ZITADEL_DESTINATION_COCKROACH_MAXIDLECONNS - MaxConnLifetime: 30m # ZITADEL_DESTINATION_COCKROACH_MAXCONNLIFETIME - MaxConnIdleTime: 5m # ZITADEL_DESTINATION_COCKROACH_MAXCONNIDLETIME - EventPushConnRatio: 0.01 # ZITADEL_DESTINATION_COCKROACH_EVENTPUSHCONNRATIO - ProjectionSpoolerConnRatio: 0.5 # ZITADEL_DESTINATION_COCKROACH_PROJECTIONSPOOLERCONNRATIO - Options: "" # ZITADEL_DESTINATION_COCKROACH_OPTIONS - User: - Username: zitadel # ZITADEL_DESTINATION_COCKROACH_USER_USERNAME - Password: "" # ZITADEL_DESTINATION_COCKROACH_USER_PASSWORD - SSL: - Mode: disable # ZITADEL_DESTINATION_COCKROACH_USER_SSL_MODE - RootCert: "" # ZITADEL_DESTINATION_COCKROACH_USER_SSL_ROOTCERT - Cert: "" # ZITADEL_DESTINATION_COCKROACH_USER_SSL_CERT - Key: "" # ZITADEL_DESTINATION_COCKROACH_USER_SSL_KEY - # Postgres is used as soon as a value is set - # The values describe the possible fields to set values postgres: - Host: # ZITADEL_DESTINATION_POSTGRES_HOST - Port: # ZITADEL_DESTINATION_POSTGRES_PORT - Database: # ZITADEL_DESTINATION_POSTGRES_DATABASE - MaxOpenConns: # ZITADEL_DESTINATION_POSTGRES_MAXOPENCONNS - MaxIdleConns: # ZITADEL_DESTINATION_POSTGRES_MAXIDLECONNS - MaxConnLifetime: # ZITADEL_DESTINATION_POSTGRES_MAXCONNLIFETIME - MaxConnIdleTime: # ZITADEL_DESTINATION_POSTGRES_MAXCONNIDLETIME - Options: # ZITADEL_DESTINATION_POSTGRES_OPTIONS + Host: localhost # ZITADEL_DATABASE_POSTGRES_HOST + Port: 5432 # ZITADEL_DATABASE_POSTGRES_PORT + Database: zitadel # ZITADEL_DATABASE_POSTGRES_DATABASE + MaxOpenConns: 5 # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS + MaxIdleConns: 2 # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS + MaxConnLifetime: 30m # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME + MaxConnIdleTime: 5m # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME + Options: "" # ZITADEL_DATABASE_POSTGRES_OPTIONS User: - Username: # ZITADEL_DESTINATION_POSTGRES_USER_USERNAME - Password: # ZITADEL_DESTINATION_POSTGRES_USER_PASSWORD + Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME + Password: "" # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD SSL: - Mode: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_MODE - RootCert: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_ROOTCERT - Cert: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_CERT - Key: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_KEY + Mode: disable # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE + RootCert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT + Cert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT + Key: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY # As cockroachdb first copies the data into memory this parameter is used to iterate through the events table and fetch only the given amount of events per iteration EventBulkSize: 10000 # ZITADEL_EVENTBULKSIZE @@ -227,6 +222,6 @@ It is not possible to use files as source or destination. See github issue [here Currently the encryption keys of the source database must be copied to the destination database. See github issue [here](https://github.com/zitadel/zitadel/issues/7964) -It is not possible to change the domain of the ZITADEL deployment. +It is not possible to change the domain of the Zitadel deployment. Once you mirrored an instance using the `--instance` flag, you have to make sure you don't mirror other preexisting instances. This means for example, you cannot mirror a few instances and then pass the `--system` flag. You have to pass all remaining instances explicitly, once you used the `--instance` flag diff --git a/docs/docs/self-hosting/manage/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/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/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 0917d4397e..81332a470a 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -11,8 +11,8 @@ const sidebar_api_settings_service_v2 = require("./docs/apis/resources/settings_ 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_v3 = require("./docs/apis/resources/action_service_v3/sidebar.ts").default -const sidebar_api_webkey_service_v3 = require("./docs/apis/resources/webkey_service_v3/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: [ @@ -592,7 +592,7 @@ module.exports = { items: [ { type: "category", - label: "V1 (Generally Available)", + label: "V1", collapsed: false, link: { type: "generated-index", @@ -656,7 +656,7 @@ module.exports = { }, { type: "category", - label: "V2 (Generally Available)", + label: "V2", collapsed: false, link: { type: "doc", @@ -665,7 +665,7 @@ module.exports = { items: [ { type: "category", - label: "User Lifecycle", + label: "User", link: { type: "generated-index", title: "User Service API", @@ -677,7 +677,7 @@ module.exports = { }, { type: "category", - label: "Session Lifecycle", + label: "Session", link: { type: "generated-index", title: "Session Service API", @@ -689,7 +689,7 @@ module.exports = { }, { type: "category", - label: "OIDC Lifecycle", + label: "OIDC", link: { type: "generated-index", title: "OIDC Service API", @@ -701,7 +701,7 @@ module.exports = { }, { type: "category", - label: "Settings Lifecycle", + label: "Settings", link: { type: "generated-index", title: "Settings Service API", @@ -713,7 +713,7 @@ module.exports = { }, { type: "category", - label: "Feature Lifecycle", + label: "Feature", link: { type: "generated-index", title: "Feature Service API", @@ -725,7 +725,7 @@ module.exports = { }, { type: "category", - label: "Organization Lifecycle", + label: "Organization", link: { type: "generated-index", title: "Organization Service API", @@ -737,7 +737,7 @@ module.exports = { }, { type: "category", - label: "Identity Provider Lifecycle", + label: "Identity Provider", link: { type: "generated-index", title: "Identity Provider Service API", @@ -747,53 +747,53 @@ module.exports = { }, items: sidebar_api_idp_service_v2, }, - ], - }, - { - type: "category", - label: "V3 (Preview)", - collapsed: false, - items: [ { type: "category", - label: "Action Lifecycle (Preview)", + label: "Web key (Beta)", link: { type: "generated-index", - title: "Action Service API (Preview)", - slug: "/apis/resources/action_service_v3", + title: "Web Key Service API (Beta)", + slug: "/apis/resources/webkey_service_v2", description: - "This API is intended to manage custom executions and targets (previously known as actions) in a ZITADEL instance.\n" + - "The version 3 of actions provide much more options to customize ZITADELs behaviour than previous action versions.\n" + - "Also, v3 actions are available instance-wide, whereas previous actions had to be managed for each organization individually\n" + - "ZITADEL doesn't restrict the implementation languages, tooling and runtime for v3 action executions anymore.\n" + - "Instead, it calls external endpoints which are implemented and maintained by action v3 users.\n" + - "\n" + - "This project is in Preview state. It can AND will continue breaking until the services provide the same functionality as the current actions.", + "This API is intended to manage 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: 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: [ { type: "doc", - id: "apis/actions/v3/usage", + id: "apis/actions/v2/usage", }, { type: "doc", - id: "apis/actions/v3/testing-locally", + id: "apis/actions/v2/testing-locally", }, - ].concat(sidebar_api_actions_v3), - }, - { - type: "category", - label: "Web key Lifecycle (Preview)", - link: { - type: "generated-index", - title: "Web Key Service API (Preview)", - slug: "/apis/resources/webkey_service_v3", - description: - "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n" + - "\n" + - "This project is in preview state. It can AND will continue breaking until a stable version is released.", - }, - items: sidebar_api_webkey_service_v3, + ].concat(sidebar_api_actions_v2), }, ], }, 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/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..96d1552d7e 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -3409,9 +3409,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" 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 b27972938e..99df9ad86f 100644 --- a/go.mod +++ b/go.mod @@ -24,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 @@ -146,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 @@ -187,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 5450c57b3d..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= 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 92% rename from internal/api/grpc/resources/action/v3alpha/execution.go rename to internal/api/grpc/action/v2beta/execution.go index 94ad17c2f0..8a7cd18ab4 100644 --- a/internal/api/grpc/resources/action/v3alpha/execution.go +++ b/internal/api/grpc/action/v2beta/execution.go @@ -3,21 +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) { @@ -56,7 +56,7 @@ 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 } diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go similarity index 78% rename from internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go rename to internal/api/grpc/action/v2beta/integration_test/execution_target_test.go index 9e8bfac3eb..6e3ab76fac 100644 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go @@ -5,12 +5,8 @@ package action_test import ( "context" "encoding/base64" - "encoding/json" - "io" "net/http" - "net/http/httptest" "net/url" - "reflect" "strings" "testing" "time" @@ -32,21 +28,17 @@ import ( "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" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" 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 ( - redirectURI = "https://callback" - logoutRedirectURI = "https://logged-out" redirectURIImplicit = "http://localhost:9999/callback" ) @@ -58,13 +50,12 @@ 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" + fullMethod := action.ActionService_GetTarget_FullMethodName tests := []struct { name string ctx context.Context - dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) (func(), error) + dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) (closeF func(), calledF func() bool) clean func(context.Context) req *action.GetTargetRequest want *action.GetTargetResponse @@ -73,7 +64,7 @@ func TestServer_ExecutionTarget(t *testing.T) { { name: "GetTarget, request and response, ok", ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) { + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) { orgID := instance.DefaultOrg.Id projectID := "" @@ -87,50 +78,55 @@ func TestServer_ExecutionTarget(t *testing.T) { // 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()} + changedRequest := &action.GetTargetRequest{Id: targetCreated.GetId()} // replace original request with different targetID - urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusOK, changedRequest) + urlRequest, closeRequest, calledRequest, _ := integration.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())) + waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.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, + Target: &action.Target{ + Id: targetCreated.GetId(), + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ + RestCall: &action.RESTCall{ InterruptOnError: false, }, }, - Timeout: durationpb.New(10 * time.Second), - Endpoint: targetCreatedURL, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), }, } + // has to be set separately because of the pointers + response.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(), + } // content for partial update changedResponse := &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Id: targetCreated.GetDetails().GetId(), + Target: &action.Target{ + Id: targetCreated.GetId(), + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, }, }, } @@ -146,14 +142,22 @@ func TestServer_ExecutionTarget(t *testing.T) { Response: expectedResponse, } // after request with different targetID, return changed response - targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusOK, changedResponse) + targetResponseURL, closeResponse, calledResponse, _ := integration.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())) + waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId())) return func() { - closeRequest() - closeResponse() - }, nil + 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)) @@ -163,38 +167,32 @@ func TestServer_ExecutionTarget(t *testing.T) { 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(), - }, - }, + Target: &action.Target{ + Id: "changed", }, }, }, { 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" + 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: request} - urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusInternalServerError, &action.GetTargetRequest{Id: "notchanged"}) + urlRequest, closeRequest, calledRequest, _ := integration.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())) + waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId())) // GetTarget with used target - request.Id = targetRequest.GetDetails().GetId() + request.Id = targetRequest.GetId() return func() { - closeRequest() - }, nil + closeRequest() + }, func() bool { + return calledRequest() == 1 + } }, clean: func(ctx context.Context) { instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) @@ -205,9 +203,7 @@ func TestServer_ExecutionTarget(t *testing.T) { { 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" + 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 @@ -219,29 +215,33 @@ func TestServer_ExecutionTarget(t *testing.T) { targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) // GetTarget with used target - request.Id = targetCreated.GetDetails().GetId() + request.Id = targetCreated.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, - }, + 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(10 * time.Second), }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), }, } // content for partial update changedResponse := &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Id: "changed", + Target: &action.Target{ + Id: "changed", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, }, }, } @@ -257,13 +257,15 @@ func TestServer_ExecutionTarget(t *testing.T) { Response: expectedResponse, } // after request with different targetID, return changed response - targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusInternalServerError, changedResponse) + targetResponseURL, closeResponse, calledResponse, _ := integration.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())) + waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId())) return func() { - closeResponse() - }, nil + closeResponse() + }, func() bool { + return calledResponse() == 1 + } }, clean: func(ctx context.Context) { instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) @@ -274,29 +276,200 @@ func TestServer_ExecutionTarget(t *testing.T) { } 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) + 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.ActionV3Alpha.GetTarget(tt.ctx, tt.req) + got, err := instance.Client.ActionV2beta.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()) + 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) + ensureFeatureEnabled(t, instance) + 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), executionTargetsSingleTarget(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) + ensureFeatureEnabled(t, instance) + 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), executionTargetsSingleTarget(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) + ensureFeatureEnabled(t, instance) + 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), executionTargetsSingleTarget(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") }) } } @@ -306,7 +479,7 @@ func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *in retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := instance.Client.ActionV3Alpha.SearchExecutions(ctx, &action.SearchExecutionsRequest{ + got, err := instance.Client.ActionV2beta.ListExecutions(ctx, &action.ListExecutionsRequest{ Filters: []*action.ExecutionSearchFilter{ {Filter: &action.ExecutionSearchFilter_InConditionsFilter{ InConditionsFilter: &action.InConditionsFilter{Conditions: []*action.Condition{condition}}, @@ -319,7 +492,7 @@ func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *in if !assert.Len(ttt, got.GetResult(), 1) { return } - gotTargets := got.GetResult()[0].GetExecution().GetTargets() + gotTargets := got.GetResult()[0].GetTargets() // always first check length, otherwise its failed anyway if assert.Len(ttt, gotTargets, len(targets)) { for i := range targets { @@ -335,10 +508,10 @@ func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Inst retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := instance.Client.ActionV3Alpha.SearchTargets(ctx, &action.SearchTargetsRequest{ + got, err := instance.Client.ActionV2beta.ListTargets(ctx, &action.ListTargetsRequest{ Filters: []*action.TargetSearchFilter{ {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ - InTargetIdsFilter: &action.InTargetIDsFilter{TargetIds: []string{resp.GetDetails().GetId()}}, + InTargetIdsFilter: &action.InTargetIDsFilter{TargetIds: []string{resp.GetId()}}, }}, }, }) @@ -348,7 +521,7 @@ func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Inst if !assert.Len(ttt, got.GetResult(), 1) { return } - config := got.GetResult()[0].GetConfig() + config := got.GetResult()[0] assert.Equal(ttt, config.GetEndpoint(), endpoint) switch ty { case domain.TargetTypeWebhook: @@ -392,50 +565,16 @@ func conditionResponseFullMethod(fullMethod string) *action.Condition { } } -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 - } +func conditionEvent(event string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Event{ + Event: event, + }, + }, + }, } - - server := httptest.NewServer(http.HandlerFunc(handler)) - - return server.URL, server.Close } func conditionFunction(function string) *action.Condition { @@ -643,10 +782,10 @@ func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *int } expectedContextInfo := contextInfoForUserOIDC(instance, "function/preuserinfo", userResp, userEmail, userPhone) - targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, response) + targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true) - waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), executionTargetsSingleTarget(targetResp.GetDetails().GetId())) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), executionTargetsSingleTarget(targetResp.GetId())) return userResp.GetUserId(), closeF } @@ -949,10 +1088,10 @@ func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance * } expectedContextInfo := contextInfoForUserOIDC(instance, "function/preaccesstoken", userResp, userEmail, userPhone) - targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, response) + targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true) - waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preaccesstoken"), executionTargetsSingleTarget(targetResp.GetDetails().GetId())) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preaccesstoken"), executionTargetsSingleTarget(targetResp.GetId())) return userResp.GetUserId(), closeF } @@ -1115,10 +1254,10 @@ func expectPreSAMLResponseExecution(ctx context.Context, t *testing.T, instance } expectedContextInfo := contextInfoForUserSAML(instance, "function/presamlresponse", userResp, userEmail, userPhone) - targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, response) + targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true) - waitForExecutionOnCondition(ctx, t, instance, conditionFunction("presamlresponse"), executionTargetsSingleTarget(targetResp.GetDetails().GetId())) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("presamlresponse"), executionTargetsSingleTarget(targetResp.GetId())) return userResp.GetUserId(), closeF } diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go b/internal/api/grpc/action/v2beta/integration_test/execution_test.go similarity index 61% rename from internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go rename to internal/api/grpc/action/v2beta/integration_test/execution_test.go index a4d4fe24f8..3af419d97b 100644 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/execution_test.go @@ -5,15 +5,14 @@ package action_test import ( "context" "testing" + "time" + "github.com/stretchr/testify/assert" "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" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType { @@ -31,11 +30,11 @@ func TestServer_SetExecution_Request(t *testing.T) { 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 string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool }{ { name: "missing permission", @@ -60,9 +59,7 @@ func TestServer_SetExecution_Request(t *testing.T) { Request: &action.RequestExecution{}, }, }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -79,9 +76,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -98,19 +93,9 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - 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(), - }, - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, + wantSetDate: true, }, { name: "service, not existing", @@ -125,9 +110,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -144,19 +127,9 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - 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(), - }, - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, + wantSetDate: true, }, { name: "all, ok", @@ -171,33 +144,24 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - 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(), - }, - }, + Targets: executionTargetsSingleTarget(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 - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) + 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) + assert.Error(t, err) return } - require.NoError(t, err) + assert.NoError(t, err) - integration.AssertResourceDetails(t, tt.want.Details, got.Details) + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) // cleanup to not impact other requests instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) @@ -205,6 +169,18 @@ func TestServer_SetExecution_Request(t *testing.T) { } } +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_Request_Include(t *testing.T) { instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) @@ -221,7 +197,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { } instance.SetExecution(isolatedIAMOwnerCTX, t, executionCond, - executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + executionTargetsSingleTarget(targetResp.GetId()), ) circularExecutionService := &action.Condition{ @@ -252,20 +228,18 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { ) tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool }{ { name: "method, circular error", ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: circularExecutionService, - Execution: &action.Execution{ - Targets: executionTargetsSingleInclude(circularExecutionMethod), - }, + Targets: executionTargetsSingleInclude(circularExecutionMethod), }, wantErr: true, }, @@ -282,19 +256,9 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }, }, }, - 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(), - }, - }, + Targets: executionTargetsSingleInclude(executionCond), }, + wantSetDate: true, }, { name: "service, ok", @@ -304,38 +268,28 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", + Service: "zitadel.user.v2beta.UserService", }, }, }, }, - 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(), - }, - }, + Targets: executionTargetsSingleInclude(executionCond), }, + 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 - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) + 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) - integration.AssertResourceDetails(t, tt.want.Details, got.Details) + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) // cleanup to not impact other requests instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) @@ -350,11 +304,11 @@ func TestServer_SetExecution_Response(t *testing.T) { 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 string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool }{ { name: "missing permission", @@ -379,9 +333,7 @@ func TestServer_SetExecution_Response(t *testing.T) { Response: &action.ResponseExecution{}, }, }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -398,9 +350,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -417,19 +367,9 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - 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(), - }, - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, + wantSetDate: true, }, { name: "service, not existing", @@ -444,9 +384,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -463,19 +401,9 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - 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(), - }, - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, + wantSetDate: true, }, { name: "all, ok", @@ -490,33 +418,23 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - 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(), - }, - }, + Targets: executionTargetsSingleTarget(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 - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) + 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) - integration.AssertResourceDetails(t, tt.want.Details, got.Details) + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) // cleanup to not impact other requests instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) @@ -531,11 +449,11 @@ func TestServer_SetExecution_Event(t *testing.T) { 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 string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool }{ { name: "missing permission", @@ -562,33 +480,27 @@ func TestServer_SetExecution_Event(t *testing.T) { Event: &action.EventExecution{}, }, }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, + Targets: executionTargetsSingleTarget(targetResp.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", - }, + { + 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, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, - */ + wantErr: true, + }, { name: "event, ok", ctx: isolatedIAMOwnerCTX, @@ -597,72 +509,65 @@ func TestServer_SetExecution_Event(t *testing.T) { ConditionType: &action.Condition_Event{ Event: &action.EventExecution{ Condition: &action.EventExecution_Event{ - Event: "xxx", + Event: "user.human.added", }, }, }, }, - 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(), - }, - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, + wantSetDate: true, }, - /* - // 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", + 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", + Group: "user.notexisting", }, }, }, }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), + 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: executionTargetsSingleTarget(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: executionTargetsSingleTarget(targetResp.GetId()), + }, + wantSetDate: true, }, { name: "all, ok", @@ -677,33 +582,23 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, }, - 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(), - }, - }, + Targets: executionTargetsSingleTarget(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 - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) + 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) - integration.AssertResourceDetails(t, tt.want.Details, got.Details) + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) // cleanup to not impact other requests instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) @@ -718,11 +613,11 @@ func TestServer_SetExecution_Function(t *testing.T) { 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 string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool }{ { name: "missing permission", @@ -747,9 +642,7 @@ func TestServer_SetExecution_Function(t *testing.T) { Response: &action.ResponseExecution{}, }, }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -762,9 +655,7 @@ func TestServer_SetExecution_Function(t *testing.T) { Function: &action.FunctionExecution{Name: "xxx"}, }, }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -777,33 +668,23 @@ func TestServer_SetExecution_Function(t *testing.T) { Function: &action.FunctionExecution{Name: "presamlresponse"}, }, }, - 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(), - }, - }, + Targets: executionTargetsSingleTarget(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 - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) + 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) - integration.AssertResourceDetails(t, tt.want.Details, got.Details) + 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/resources/action/v3alpha/integration_test/query_test.go b/internal/api/grpc/action/v2beta/integration_test/query_test.go similarity index 50% rename from internal/api/grpc/resources/action/v3alpha/integration_test/query_test.go rename to internal/api/grpc/action/v2beta/integration_test/query_test.go index b46870d98c..c5159d39da 100644 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/query_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/query_test.go @@ -4,7 +4,6 @@ package action_test import ( "context" - "reflect" "testing" "time" @@ -12,13 +11,11 @@ import ( "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" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" ) func TestServer_GetTarget(t *testing.T) { @@ -59,27 +56,23 @@ func TestServer_GetTarget(t *testing.T) { 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() + 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.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), + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, }, + Timeout: durationpb.New(5 * time.Second), }, }, }, @@ -90,27 +83,23 @@ func TestServer_GetTarget(t *testing.T) { 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() + 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.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), + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.RESTAsync{}, }, + Timeout: durationpb.New(5 * time.Second), }, }, }, @@ -121,29 +110,25 @@ func TestServer_GetTarget(t *testing.T) { 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() + 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.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, - }, + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: true, }, - Timeout: durationpb.New(10 * time.Second), }, + Timeout: durationpb.New(5 * time.Second), }, }, }, @@ -154,29 +139,25 @@ func TestServer_GetTarget(t *testing.T) { 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() + 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.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, - }, + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, }, - Timeout: durationpb.New(10 * time.Second), }, + Timeout: durationpb.New(5 * time.Second), }, }, }, @@ -187,29 +168,25 @@ func TestServer_GetTarget(t *testing.T) { 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() + 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.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, - }, + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, }, - Timeout: durationpb.New(10 * time.Second), }, + Timeout: durationpb.New(5 * time.Second), }, }, }, @@ -222,20 +199,13 @@ func TestServer_GetTarget(t *testing.T) { } 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) + got, err := instance.Client.ActionV2beta.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()) + assert.NoError(ttt, err) + assert.EqualExportedValues(ttt, tt.want, got) }, retryDuration, tick, "timeout waiting for expected target result") }) } @@ -247,20 +217,20 @@ func TestServer_ListTargets(t *testing.T) { isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) type args struct { ctx context.Context - dep func(context.Context, *action.SearchTargetsRequest, *action.SearchTargetsResponse) error - req *action.SearchTargetsRequest + dep func(context.Context, *action.ListTargetsRequest, *action.ListTargetsResponse) + req *action.ListTargetsRequest } tests := []struct { name string args args - want *action.SearchTargetsResponse + want *action.ListTargetsResponse wantErr bool }{ { name: "missing permission", args: args{ ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SearchTargetsRequest{}, + req: &action.ListTargetsRequest{}, }, wantErr: true, }, @@ -268,7 +238,7 @@ func TestServer_ListTargets(t *testing.T) { name: "list, not found", args: args{ ctx: isolatedIAMOwnerCTX, - req: &action.SearchTargetsRequest{ + req: &action.ListTargetsRequest{ Filters: []*action.TargetSearchFilter{ {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ InTargetIdsFilter: &action.InTargetIDsFilter{ @@ -279,56 +249,51 @@ func TestServer_ListTargets(t *testing.T) { }, }, }, - want: &action.SearchTargetsResponse{ - Details: &resource_object.ListDetails{ + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ TotalResult: 0, AppliedLimit: 100, }, - Result: []*action.GetTarget{}, + Result: []*action.Target{}, }, }, { name: "list single id", args: args{ ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error { + 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.GetDetails().GetId()}, + TargetIds: []string{resp.GetId()}, }, } - response.Details.Timestamp = resp.GetDetails().GetChanged() - response.Result[0].Details = resp.GetDetails() - response.Result[0].Config.Name = name - return nil + 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.SearchTargetsRequest{ + req: &action.ListTargetsRequest{ Filters: []*action.TargetSearchFilter{{}}, }, }, - want: &action.SearchTargetsResponse{ - Details: &resource_object.ListDetails{ + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ TotalResult: 1, AppliedLimit: 100, }, - Result: []*action.GetTarget{ + Result: []*action.Target{ { - 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, - }, + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, }, - Timeout: durationpb.New(10 * time.Second), }, + Timeout: durationpb.New(5 * time.Second), }, }, }, @@ -336,7 +301,7 @@ func TestServer_ListTargets(t *testing.T) { name: "list single name", args: args{ ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error { + 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{ @@ -344,40 +309,31 @@ func TestServer_ListTargets(t *testing.T) { TargetName: name, }, } - response.Details.Timestamp = resp.GetDetails().GetChanged() - response.Result[0].Details = resp.GetDetails() - response.Result[0].Config.Name = name - return nil + 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.SearchTargetsRequest{ + req: &action.ListTargetsRequest{ Filters: []*action.TargetSearchFilter{{}}, }, }, - want: &action.SearchTargetsResponse{ - Details: &resource_object.ListDetails{ + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ TotalResult: 1, AppliedLimit: 100, }, - Result: []*action.GetTarget{ + Result: []*action.Target{ { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, }, }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, + Timeout: durationpb.New(5 * time.Second), }, }, }, @@ -386,7 +342,7 @@ func TestServer_ListTargets(t *testing.T) { name: "list multiple id", args: args{ ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error { + dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) { name1 := gofakeit.Name() name2 := gofakeit.Name() name3 := gofakeit.Name() @@ -395,83 +351,62 @@ func TestServer_ListTargets(t *testing.T) { 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()}, + TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.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 + 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.SearchTargetsRequest{ + req: &action.ListTargetsRequest{ Filters: []*action.TargetSearchFilter{{}}, }, }, - want: &action.SearchTargetsResponse{ - Details: &resource_object.ListDetails{ + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ TotalResult: 3, AppliedLimit: 100, }, - Result: []*action.GetTarget{ + Result: []*action.Target{ { - 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), + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.RESTAsync{}, }, + Timeout: durationpb.New(5 * time.Second), }, { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, }, }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, + Timeout: durationpb.New(5 * time.Second), }, { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, }, }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, + Timeout: durationpb.New(5 * time.Second), }, }, }, @@ -480,13 +415,12 @@ func TestServer_ListTargets(t *testing.T) { 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) + 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.ActionV3Alpha.SearchTargets(tt.args.ctx, tt.args.req) + got, listErr := instance.Client.ActionV2beta.ListTargets(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(ttt, listErr, "Error: "+listErr.Error()) return @@ -496,18 +430,21 @@ func TestServer_ListTargets(t *testing.T) { // 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()) + assert.EqualExportedValues(ttt, tt.want.Result[i], got.Result[i]) } } - integration.AssertResourceListDetails(ttt, tt.want, got) + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) }, retryDuration, tick, "timeout waiting for expected execution result") }) } } -func TestServer_SearchExecutions(t *testing.T) { +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) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -515,20 +452,20 @@ func TestServer_SearchExecutions(t *testing.T) { type args struct { ctx context.Context - dep func(context.Context, *action.SearchExecutionsRequest, *action.SearchExecutionsResponse) error - req *action.SearchExecutionsRequest + dep func(context.Context, *action.ListExecutionsRequest, *action.ListExecutionsResponse) + req *action.ListExecutionsRequest } tests := []struct { name string args args - want *action.SearchExecutionsResponse + want *action.ListExecutionsResponse wantErr bool }{ { name: "missing permission", args: args{ ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SearchExecutionsRequest{}, + req: &action.ListExecutionsRequest{}, }, wantErr: true, }, @@ -536,17 +473,16 @@ func TestServer_SearchExecutions(t *testing.T) { name: "list request single condition", args: args{ ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0] - resp := instance.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetDetails().GetId())) + resp := instance.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetId())) - response.Details.Timestamp = resp.GetDetails().GetChanged() // Set expected response with used values for SetExecution - response.Result[0].Details = resp.GetDetails() + response.Result[0].CreationDate = resp.GetSetDate() + response.Result[0].ChangeDate = resp.GetSetDate() response.Result[0].Condition = cond - return nil }, - req: &action.SearchExecutionsRequest{ + req: &action.ListExecutionsRequest{ Filters: []*action.ExecutionSearchFilter{{ Filter: &action.ExecutionSearchFilter_InConditionsFilter{ InConditionsFilter: &action.InConditionsFilter{ @@ -564,17 +500,13 @@ func TestServer_SearchExecutions(t *testing.T) { }}, }, }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ TotalResult: 1, AppliedLimit: 100, }, - Result: []*action.GetExecution{ + Result: []*action.Execution{ { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, Condition: &action.Condition{ ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ @@ -584,9 +516,7 @@ func TestServer_SearchExecutions(t *testing.T) { }, }, }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, }, }, @@ -595,13 +525,13 @@ func TestServer_SearchExecutions(t *testing.T) { name: "list request single target", args: args{ ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { + 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.GetDetails().GetId(), + TargetId: target.GetId(), }, }, } @@ -614,35 +544,27 @@ func TestServer_SearchExecutions(t *testing.T) { }, }, } - targets := executionTargetsSingleTarget(target.GetDetails().GetId()) + targets := executionTargetsSingleTarget(target.GetId()) resp := instance.SetExecution(ctx, t, cond, targets) - response.Details.Timestamp = resp.GetDetails().GetChanged() - - response.Result[0].Details = resp.GetDetails() + response.Result[0].CreationDate = resp.GetSetDate() + response.Result[0].ChangeDate = resp.GetSetDate() response.Result[0].Condition = cond - response.Result[0].Execution.Targets = targets - return nil + response.Result[0].Targets = targets }, - req: &action.SearchExecutionsRequest{ + req: &action.ListExecutionsRequest{ Filters: []*action.ExecutionSearchFilter{{}}, }, }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ TotalResult: 1, AppliedLimit: 100, }, - Result: []*action.GetExecution{ + Result: []*action.Execution{ { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, Condition: &action.Condition{}, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(""), - }, + Targets: executionTargetsSingleTarget(""), }, }, }, @@ -650,7 +572,7 @@ func TestServer_SearchExecutions(t *testing.T) { name: "list request single include", args: args{ ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { cond := &action.Condition{ ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ @@ -660,7 +582,7 @@ func TestServer_SearchExecutions(t *testing.T) { }, }, } - instance.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetDetails().GetId())) + instance.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetId())) request.Filters[0].GetIncludeFilter().Include = cond includeCond := &action.Condition{ @@ -675,16 +597,14 @@ func TestServer_SearchExecutions(t *testing.T) { 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, + response.Result[0] = &action.Execution{ + Condition: includeCond, + CreationDate: resp2.GetSetDate(), + ChangeDate: resp2.GetSetDate(), + Targets: includeTargets, } - return nil }, - req: &action.SearchExecutionsRequest{ + req: &action.ListExecutionsRequest{ Filters: []*action.ExecutionSearchFilter{{ Filter: &action.ExecutionSearchFilter_IncludeFilter{ IncludeFilter: &action.IncludeFilter{}, @@ -692,18 +612,13 @@ func TestServer_SearchExecutions(t *testing.T) { }}, }, }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ TotalResult: 1, AppliedLimit: 100, }, - Result: []*action.GetExecution{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - }, + Result: []*action.Execution{ + {}, }, }, }, @@ -711,94 +626,81 @@ func TestServer_SearchExecutions(t *testing.T) { name: "list multiple conditions", args: args{ ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { - 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{{ + 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/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/CreateSession", }, }, - }, - { - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/SetSession", - }, + }}, + {ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/SetSession", }, }, - }, + }}, }, }, }, - }}, + } + + cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0] + targets1 := executionTargetsSingleTarget(targetResp.GetId()) + resp1 := instance.SetExecution(ctx, t, cond1, targets1) + response.Result[2] = &action.Execution{ + CreationDate: resp1.GetSetDate(), + ChangeDate: resp1.GetSetDate(), + Condition: cond1, + Targets: targets1, + } + + cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1] + targets2 := executionTargetsSingleTarget(targetResp.GetId()) + resp2 := instance.SetExecution(ctx, t, cond2, targets2) + response.Result[1] = &action.Execution{ + CreationDate: resp2.GetSetDate(), + ChangeDate: resp2.GetSetDate(), + Condition: cond2, + Targets: targets2, + } + + cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2] + targets3 := executionTargetsSingleTarget(targetResp.GetId()) + resp3 := instance.SetExecution(ctx, t, cond3, targets3) + response.Result[0] = &action.Execution{ + CreationDate: resp3.GetSetDate(), + ChangeDate: resp3.GetSetDate(), + Condition: cond3, + Targets: targets3, + } + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{ + {}, + }, }, }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ 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()}, - }, - }, + Result: []*action.Execution{ + {}, {}, {}, }, }, }, @@ -806,22 +708,20 @@ func TestServer_SearchExecutions(t *testing.T) { 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() { + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + targets := executionTargetsSingleTarget(targetResp.GetId()) + conditions := request.Filters[0].GetInConditionsFilter().GetConditions() + for i, cond := range conditions { 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, + response.Result[(len(conditions)-1)-i] = &action.Execution{ + CreationDate: resp.GetSetDate(), + ChangeDate: resp.GetSetDate(), + Condition: cond, + Targets: targets, } - // filled with info of last sequence - response.Details.Timestamp = resp.GetDetails().GetChanged() } - - return nil }, - req: &action.SearchExecutionsRequest{ + req: &action.ListExecutionsRequest{ Filters: []*action.ExecutionSearchFilter{{ Filter: &action.ExecutionSearchFilter_InConditionsFilter{ InConditionsFilter: &action.InConditionsFilter{ @@ -842,22 +742,22 @@ func TestServer_SearchExecutions(t *testing.T) { }}, }, }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ 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()}}}, + Result: []*action.Execution{ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, }, }, }, @@ -865,13 +765,12 @@ func TestServer_SearchExecutions(t *testing.T) { 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) + 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.ActionV3Alpha.SearchExecutions(tt.args.ctx, tt.args.req) + got, listErr := instance.Client.ActionV2beta.ListExecutions(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(ttt, listErr, "Error: "+listErr.Error()) return @@ -879,27 +778,19 @@ func TestServer_SearchExecutions(t *testing.T) { 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]) - } - } + assert.EqualExportedValues(ttt, got.Result, tt.want.Result) } - integration.AssertResourceListDetails(ttt, tt.want, got) + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) }, 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 +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 0, false + return false } diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/server_test.go b/internal/api/grpc/action/v2beta/integration_test/server_test.go similarity index 90% rename from internal/api/grpc/resources/action/v3alpha/integration_test/server_test.go rename to internal/api/grpc/action/v2beta/integration_test/server_test.go index bc8e43eafc..89a33dd40e 100644 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/server_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/server_test.go @@ -13,8 +13,8 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/integration" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" ) var ( @@ -60,7 +60,7 @@ func ensureFeatureEnabled(t *testing.T, instance *integration.Instance) { retryDuration, tick = integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) require.EventuallyWithT(t, func(ttt *assert.CollectT) { - _, err := instance.Client.ActionV3Alpha.ListExecutionMethods(ctx, &action.ListExecutionMethodsRequest{}) + _, err := instance.Client.ActionV2beta.ListExecutionMethods(ctx, &action.ListExecutionMethodsRequest{}) assert.NoError(ttt, err) }, retryDuration, 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..2fda64b86a --- /dev/null +++ b/internal/api/grpc/action/v2beta/integration_test/target_test.go @@ -0,0 +1,553 @@ +//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) + ensureFeatureEnabled(t, instance) + 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) + ensureFeatureEnabled(t, instance) + 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) + ensureFeatureEnabled(t, instance) + 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 81% rename from internal/api/grpc/resources/action/v3alpha/query.go rename to internal/api/grpc/action/v2beta/query.go index 7cdedd8134..d8d6cd3e95 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 ( @@ -45,11 +45,11 @@ type Context interface { GetOwner() InstanceContext } -func (s *Server) SearchTargets(ctx context.Context, req *action.SearchTargetsRequest) (*action.SearchTargetsResponse, error) { +func (s *Server) ListTargets(ctx context.Context, req *action.ListTargetsRequest) (*action.ListTargetsResponse, error) { if err := checkActionsEnabled(ctx); err != nil { return nil, err } - queries, err := s.searchTargetsRequestToModel(req) + queries, err := s.ListTargetsRequestToModel(req) if err != nil { return nil, err } @@ -57,17 +57,17 @@ 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) { +func (s *Server) ListExecutions(ctx context.Context, req *action.ListExecutionsRequest) (*action.ListExecutionsResponse, error) { if err := checkActionsEnabled(ctx); err != nil { return nil, err } - queries, err := s.searchExecutionsRequestToModel(req) + queries, err := s.ListExecutionsRequestToModel(req) if err != nil { return nil, err } @@ -75,45 +75,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 +160,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 +215,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 } @@ -319,15 +324,15 @@ 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 { +func executionToPb(e *query.Execution) *action.Execution { targets := make([]*action.ExecutionTargetType, len(e.Targets)) for i := range e.Targets { switch e.Targets[i].Type { @@ -342,12 +347,17 @@ func executionToPb(e *query.Execution) *action.GetExecution { } } - 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 79% rename from internal/api/grpc/resources/action/v3alpha/server.go rename to internal/api/grpc/action/v2beta/server.go index b80c60d668..069f456ceb 100644 --- a/internal/api/grpc/resources/action/v3alpha/server.go +++ b/internal/api/grpc/action/v2beta/server.go @@ -11,13 +11,13 @@ import ( "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,23 +47,23 @@ 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 + return action.RegisterActionServiceHandler } func checkActionsEnabled(ctx context.Context) error { diff --git a/internal/api/grpc/resources/action/v3alpha/target.go b/internal/api/grpc/action/v2beta/target.go similarity index 54% rename from internal/api/grpc/resources/action/v3alpha/target.go rename to internal/api/grpc/action/v2beta/target.go index 621b6677b7..7dc636f29a 100644 --- a/internal/api/grpc/resources/action/v3alpha/target.go +++ b/internal/api/grpc/action/v2beta/target.go @@ -4,14 +4,13 @@ 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) { @@ -20,29 +19,38 @@ func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetReque } 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) { +func (s *Server) UpdateTarget(ctx context.Context, req *action.UpdateTargetRequest) (*action.UpdateTargetResponse, error) { if err := checkActionsEnabled(ctx); err != nil { return nil, err } 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 } @@ -51,74 +59,76 @@ func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetReque 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/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/integration_test/feature_test.go b/internal/api/grpc/feature/v2/integration_test/feature_test.go index 2af4f642c4..8d6c295350 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{ @@ -349,14 +341,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{ 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/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/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/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/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 f39212f7e3..bf396fd25d 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -31,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) { @@ -46,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) @@ -1306,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) @@ -3048,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/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/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/scim/integration_test/users_list_test.go b/internal/api/scim/integration_test/users_list_test.go index 47030d37e3..8c6ccb80ef 100644 --- a/internal/api/scim/integration_test/users_list_test.go +++ b/internal/api/scim/integration_test/users_list_test.go @@ -5,444 +5,438 @@ 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]) { - // sort the created users with usernames instead of creation date - sortedResources := sortScimUserByUsername(resp.Resources) + { + 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 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) - 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: ")) - assert.True(t, strings.HasPrefix(sortedResources[1].UserName, "scim-username-2: ")) + 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"`), + { + 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")) + } + }, }, - 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) + { + 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) - 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.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: "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) + { + 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) - 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.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), - } + { + 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) + }, }, - 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 active filter", - req: &scim.ListRequest{ - Filter: gu.Ptr(`active eq false`), + { + 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") + }, }, - 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 invalid operator", + req: &scim.ListRequest{ + Filter: gu.Ptr(`externalid pr`), + }, + wantErr: true, + errorType: "invalidFilter", }, - }, - { - name: "list users with externalid filter", - req: &scim.ListRequest{ - Filter: gu.Ptr(`externalid eq "701984"`), + { + 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") + }, }, - 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, - - 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{ + { + 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 { 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/command.go b/internal/command/command.go index f9c78fbaab..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,10 +179,15 @@ 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.ActionFunctionExists(), @@ -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_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/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/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/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/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 372c224c6c..e8f761d410 100644 --- a/internal/eventstore/v3/field.go +++ b/internal/eventstore/v3/field.go @@ -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_test.go b/internal/execution/execution_test.go index 5a45d96625..40731a840a 100644 --- a/internal/execution/execution_test.go +++ b/internal/execution/execution_test.go @@ -61,7 +61,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\"}"), 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/id/sonyflake.go b/internal/id/sonyflake.go index 22a3247874..cc7086aa66 100644 --- a/internal/id/sonyflake.go +++ b/internal/id/sonyflake.go @@ -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 } diff --git a/internal/integration/action.go b/internal/integration/action.go new file mode 100644 index 0000000000..b8f69c5788 --- /dev/null +++ b/internal/integration/action.go @@ -0,0 +1,102 @@ +package integration + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "reflect" + "sync" + "time" +) + +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 +} diff --git a/internal/integration/client.go b/internal/integration/client.go index 47458cf4cd..2cb8fa3641 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -19,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" @@ -32,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" @@ -44,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 { @@ -61,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 @@ -94,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), @@ -721,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{ + 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/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..a2d4d4140e 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -6,7 +6,6 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/notification/handlers" @@ -18,7 +17,6 @@ import ( var ( projections []*handler.Handler - worker *handlers.NotificationWorker ) func Register( @@ -35,7 +33,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 +56,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) { 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..0a2a989918 100644 --- a/internal/query/execution.go +++ b/internal/query/execution.go @@ -101,8 +101,8 @@ func (q *Queries) SearchExecutions(ctx context.Context, queries *ExecutionSearch eq := sq.Eq{ ExecutionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } - query, scan := prepareExecutionsQuery(ctx, q.client) - return genericRowsQueryWithState[*Executions](ctx, q.client, executionTable, combineToWhereStmt(query, queries.toQuery, eq), scan) + query, scan := prepareExecutionsQuery() + return genericRowsQueryWithState(ctx, q.client, executionTable, combineToWhereStmt(query, queries.toQuery, eq), scan) } func (q *Queries) GetExecutionByID(ctx context.Context, id string) (execution *Execution, err error) { @@ -110,8 +110,8 @@ func (q *Queries) GetExecutionByID(ctx context.Context, id string) (execution *E ExecutionColumnID.identifier(): id, ExecutionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } - query, scan := prepareExecutionQuery(ctx, q.client) - return genericRowQuery[*Execution](ctx, q.client, query.Where(eq), scan) + query, scan := prepareExecutionQuery() + return genericRowQuery(ctx, q.client, query.Where(eq), scan) } func NewExecutionInIDsSearchQuery(values []string) (SearchQuery, error) { @@ -219,7 +219,7 @@ func (q *Queries) TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string return execution, err } -func prepareExecutionQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) { +func prepareExecutionQuery() (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) { return sq.Select( ExecutionColumnInstanceID.identifier(), ExecutionColumnID.identifier(), @@ -235,7 +235,7 @@ func prepareExecutionQuery(context.Context, prepareDatabase) (sq.SelectBuilder, scanExecution } -func prepareExecutionsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) { +func prepareExecutionsQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) { return sq.Select( ExecutionColumnInstanceID.identifier(), ExecutionColumnID.identifier(), diff --git a/internal/query/execution_test.go b/internal/query/execution_test.go index ee6bdc4d96..eaaac1e9ba 100644 --- a/internal/query/execution_test.go +++ b/internal/query/execution_test.go @@ -263,7 +263,7 @@ func Test_ExecutionPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/failed_events.go b/internal/query/failed_events.go index 7d2e875cee..c5ad1ae1d9 100644 --- a/internal/query/failed_events.go +++ b/internal/query/failed_events.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -83,7 +82,7 @@ type FailedEventSearchQueries struct { } func (q *Queries) SearchFailedEvents(ctx context.Context, queries *FailedEventSearchQueries) (failedEvents *FailedEvents, err error) { - query, scan := prepareFailedEventsQuery(ctx, q.client) + query, scan := prepareFailedEventsQuery() stmt, args, err := queries.toQuery(query).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-n8rjJ", "Errors.Query.InvalidRequest") @@ -139,7 +138,7 @@ func (q *FailedEventSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuil return query } -func prepareFailedEventsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*FailedEvents, error)) { +func prepareFailedEventsQuery() (sq.SelectBuilder, func(*sql.Rows) (*FailedEvents, error)) { return sq.Select( FailedEventsColumnProjectionName.identifier(), FailedEventsColumnFailedSequence.identifier(), @@ -149,7 +148,7 @@ func prepareFailedEventsQuery(ctx context.Context, db prepareDatabase) (sq.Selec FailedEventsColumnLastFailed.identifier(), FailedEventsColumnError.identifier(), countColumn.identifier()). - From(failedEventsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(failedEventsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*FailedEvents, error) { failedEvents := make([]*FailedEvent, 0) diff --git a/internal/query/failed_events_test.go b/internal/query/failed_events_test.go index 7e575b5891..25c15e9b8f 100644 --- a/internal/query/failed_events_test.go +++ b/internal/query/failed_events_test.go @@ -19,8 +19,7 @@ var ( ` projections.failed_events2.last_failed,` + ` projections.failed_events2.error,` + ` COUNT(*) OVER ()` + - ` FROM projections.failed_events2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.failed_events2` prepareFailedEventsCols = []string{ "projection_name", @@ -168,7 +167,7 @@ func Test_FailedEventsPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/generic.go b/internal/query/generic.go index 70fc2884a2..ea2257c013 100644 --- a/internal/query/generic.go +++ b/internal/query/generic.go @@ -44,7 +44,7 @@ func genericRowsQueryWithState[R Stateful]( scan func(rows *sql.Rows) (R, error), ) (resp R, err error) { var rnil R - resp, err = genericRowsQuery[R](ctx, client, query, scan) + resp, err = genericRowsQuery(ctx, client, query, scan) if err != nil { return rnil, err } @@ -60,7 +60,7 @@ func latestState(ctx context.Context, client *database.DB, projections ...table) ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLatestState(ctx, client) + query, scan := prepareLatestState() or := make(sq.Or, len(projections)) for i, projection := range projections { or[i] = sq.Eq{CurrentStateColProjectionName.identifier(): projection.name} diff --git a/internal/query/iam_member.go b/internal/query/iam_member.go index 87b906aa51..139208c7b8 100644 --- a/internal/query/iam_member.go +++ b/internal/query/iam_member.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -71,7 +70,7 @@ func (q *Queries) IAMMembers(ctx context.Context, queries *IAMMembersQuery) (mem ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareInstanceMembersQuery(ctx, q.client) + query, scan := prepareInstanceMembersQuery() eq := sq.Eq{InstanceMemberInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -94,7 +93,7 @@ func (q *Queries) IAMMembers(ctx context.Context, queries *IAMMembersQuery) (mem return members, err } -func prepareInstanceMembersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { +func prepareInstanceMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { return sq.Select( InstanceMemberCreationDate.identifier(), InstanceMemberChangeDate.identifier(), @@ -116,7 +115,7 @@ func prepareInstanceMembersQuery(ctx context.Context, db prepareDatabase) (sq.Se LeftJoin(join(HumanUserIDCol, InstanceMemberUserID)). LeftJoin(join(MachineUserIDCol, InstanceMemberUserID)). LeftJoin(join(UserIDCol, InstanceMemberUserID)). - LeftJoin(join(LoginNameUserIDCol, InstanceMemberUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, InstanceMemberUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/iam_member_test.go b/internal/query/iam_member_test.go index 82cea360c8..5c10ebc5bc 100644 --- a/internal/query/iam_member_test.go +++ b/internal/query/iam_member_test.go @@ -39,7 +39,6 @@ var ( "ON members.user_id = projections.users14.id AND members.instance_id = projections.users14.instance_id " + "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id AND members.instance_id = projections.login_names3.instance_id " + - "AS OF SYSTEM TIME '-1 ms' " + "WHERE projections.login_names3.is_primary = $1") instanceMembersColumns = []string{ "creation_date", @@ -295,7 +294,7 @@ func Test_IAMMemberPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/idp.go b/internal/query/idp.go index 2687397330..4f0d77c62b 100644 --- a/internal/query/idp.go +++ b/internal/query/idp.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" @@ -215,7 +214,7 @@ func (q *Queries) IDPByIDAndResourceOwner(ctx context.Context, shouldTriggerBulk sq.Eq{IDPResourceOwnerCol.identifier(): authz.GetInstance(ctx).InstanceID()}, }, } - stmt, scan := prepareIDPByIDQuery(ctx, q.client) + stmt, scan := prepareIDPByIDQuery() query, args, err := stmt.Where(where).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-0gocI", "Errors.Query.SQLStatement") @@ -233,7 +232,7 @@ func (q *Queries) IDPs(ctx context.Context, queries *IDPSearchQueries, withOwner ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareIDPsQuery(ctx, q.client) + query, scan := prepareIDPsQuery() eq := sq.Eq{ IDPInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -293,7 +292,7 @@ func (q *IDPSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { return query } -func prepareIDPByIDQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*IDP, error)) { +func prepareIDPByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDP, error)) { return sq.Select( IDPIDCol.identifier(), IDPResourceOwnerCol.identifier(), @@ -321,7 +320,7 @@ func prepareIDPByIDQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil JWTIDPColEndpoint.identifier(), ).From(idpTable.identifier()). LeftJoin(join(OIDCIDPColIDPID, IDPIDCol)). - LeftJoin(join(JWTIDPColIDPID, IDPIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(JWTIDPColIDPID, IDPIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*IDP, error) { idp := new(IDP) @@ -401,7 +400,7 @@ func prepareIDPByIDQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } } -func prepareIDPsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPs, error)) { +func prepareIDPsQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPs, error)) { return sq.Select( IDPIDCol.identifier(), IDPResourceOwnerCol.identifier(), @@ -430,7 +429,7 @@ func prepareIDPsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder countColumn.identifier(), ).From(idpTable.identifier()). LeftJoin(join(OIDCIDPColIDPID, IDPIDCol)). - LeftJoin(join(JWTIDPColIDPID, IDPIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(JWTIDPColIDPID, IDPIDCol)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPs, error) { idps := make([]*IDP, 0) diff --git a/internal/query/idp_login_policy_link.go b/internal/query/idp_login_policy_link.go index bdc2ef15b1..65f855bc51 100644 --- a/internal/query/idp_login_policy_link.go +++ b/internal/query/idp_login_policy_link.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -97,7 +96,7 @@ func (q *Queries) IDPLoginPolicyLinks(ctx context.Context, resourceOwner string, ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareIDPLoginPolicyLinksQuery(ctx, q.client, resourceOwner) + query, scan := prepareIDPLoginPolicyLinksQuery(ctx, resourceOwner) eq := sq.Eq{ IDPLoginPolicyLinkInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -122,7 +121,8 @@ func (q *Queries) IDPLoginPolicyLinks(ctx context.Context, resourceOwner string, return idps, err } -func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase, resourceOwner string) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { +//nolint:gocognit +func prepareIDPLoginPolicyLinksQuery(ctx context.Context, resourceOwner string) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { resourceOwnerQuery, resourceOwnerArgs, err := prepareIDPLoginPolicyLinksResourceOwnerQuery(ctx, resourceOwner) if err != nil { return sq.SelectBuilder{}, nil @@ -142,8 +142,7 @@ func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase, re LeftJoin(join(IDPTemplateIDCol, IDPLoginPolicyLinkIDPIDCol)). RightJoin("("+resourceOwnerQuery+") AS "+idpLoginPolicyOwnerTable.alias+" ON "+ idpLoginPolicyOwnerIDCol.identifier()+" = "+IDPLoginPolicyLinkResourceOwnerCol.identifier()+" AND "+ - idpLoginPolicyOwnerInstanceIDCol.identifier()+" = "+IDPLoginPolicyLinkInstanceIDCol.identifier()+ - " "+db.Timetravel(call.Took(ctx)), + idpLoginPolicyOwnerInstanceIDCol.identifier()+" = "+IDPLoginPolicyLinkInstanceIDCol.identifier(), resourceOwnerArgs...). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPLoginPolicyLinks, error) { diff --git a/internal/query/idp_login_policy_link_test.go b/internal/query/idp_login_policy_link_test.go index 245eb22ccc..9f66e118ea 100644 --- a/internal/query/idp_login_policy_link_test.go +++ b/internal/query/idp_login_policy_link_test.go @@ -6,6 +6,7 @@ import ( "database/sql/driver" "errors" "fmt" + "reflect" "regexp" "testing" @@ -29,8 +30,7 @@ var ( ` LEFT JOIN projections.idp_templates6 ON projections.idp_login_policy_links5.idp_id = projections.idp_templates6.id AND projections.idp_login_policy_links5.instance_id = projections.idp_templates6.instance_id` + ` RIGHT JOIN (SELECT login_policy_owner.aggregate_id, login_policy_owner.instance_id, login_policy_owner.owner_removed FROM projections.login_policies5 AS login_policy_owner` + ` WHERE (login_policy_owner.instance_id = $1 AND (login_policy_owner.aggregate_id = $2 OR login_policy_owner.aggregate_id = $3)) ORDER BY login_policy_owner.is_default LIMIT 1) AS login_policy_owner` + - ` ON login_policy_owner.aggregate_id = projections.idp_login_policy_links5.resource_owner AND login_policy_owner.instance_id = projections.idp_login_policy_links5.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` ON login_policy_owner.aggregate_id = projections.idp_login_policy_links5.resource_owner AND login_policy_owner.instance_id = projections.idp_login_policy_links5.instance_id`) loginPolicyIDPLinksCols = []string{ "idp_id", "name", @@ -52,14 +52,14 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { } tests := []struct { name string - prepare interface{} + prepare any want want - object interface{} + object any }{ { name: "prepareIDPsQuery found", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { - return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner") + prepare: func(ctx context.Context) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + return prepareIDPLoginPolicyLinksQuery(ctx, "resourceOwner") }, want: want{ sqlExpectations: mockQueries( @@ -101,8 +101,8 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { }, { name: "prepareIDPsQuery no idp", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { - return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner") + prepare: func(ctx context.Context) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + return prepareIDPLoginPolicyLinksQuery(ctx, "resourceOwner") }, want: want{ sqlExpectations: mockQueries( @@ -143,8 +143,8 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { }, { name: "prepareIDPsQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { - return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner") + prepare: func(ctx context.Context) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + return prepareIDPLoginPolicyLinksQuery(ctx, "resourceOwner") }, want: want{ sqlExpectations: mockQueryErr( @@ -163,7 +163,7 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, reflect.ValueOf(context.Background())) }) } } diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index 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_test.go b/internal/query/instance_features_test.go index e182f4002f..903c2872a9 100644 --- a/internal/query/instance_features_test.go +++ b/internal/query/instance_features_test.go @@ -84,28 +84,28 @@ 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]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceActionsEventType, false, )), @@ -141,28 +141,28 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { { 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]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceActionsEventType, false, )), @@ -170,7 +170,7 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, )), @@ -207,23 +207,23 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { 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]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceActionsEventType, false, )), @@ -231,7 +231,7 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, )), 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 e1792da945..3d9a2c33b3 100644 --- a/internal/query/message_text.go +++ b/internal/query/message_text.go @@ -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) 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/projection.go b/internal/query/projection/projection.go index d6647d0961..f4e3bbe0d4 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -86,6 +86,7 @@ var ( OrgDomainVerifiedFields *handler.FieldHandler InstanceDomainFields *handler.FieldHandler MembershipFields *handler.FieldHandler + PermissionFields *handler.FieldHandler ) type projection interface { @@ -97,6 +98,7 @@ type projection interface { var ( projections []projection + fields []*handler.FieldHandler ) func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, config Config, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, certEncryptionAlgorithm crypto.EncryptionAlgorithm, systemUsers map[string]*internal_authz.SystemAPIUser) error { @@ -176,8 +178,11 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, OrgDomainVerifiedFields = newFillOrgDomainVerifiedFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsOrgDomainVerified])) InstanceDomainFields = newFillInstanceDomainFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsInstanceDomain])) MembershipFields = newFillMembershipFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsMemberships])) + PermissionFields = newFillPermissionFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsPermission])) + // Don't forget to add the new field handler to [ProjectInstanceFields] newProjectionsList() + newFieldsList() return nil } @@ -210,6 +215,16 @@ func ProjectInstance(ctx context.Context) error { return nil } +func ProjectInstanceFields(ctx context.Context) error { + for _, fieldProjection := range fields { + err := fieldProjection.Trigger(ctx) + if err != nil { + return err + } + } + return nil +} + func ApplyCustomConfig(customConfig CustomConfig) handler.Config { return applyCustomConfig(projectionConfig, customConfig) } @@ -234,6 +249,16 @@ func applyCustomConfig(config handler.Config, customConfig CustomConfig) handler return config } +func newFieldsList() { + fields = []*handler.FieldHandler{ + ProjectGrantFields, + OrgDomainVerifiedFields, + InstanceDomainFields, + MembershipFields, + PermissionFields, + } +} + // we know this is ugly, but we need to have a singleton slice of all projections // and are only able to initialize it after all projections are created // as setup and start currently create them individually, we make sure we get the right one diff --git a/internal/query/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 a81a1b2c34..784627bc59 100644 --- a/internal/query/saml_request.go +++ b/internal/query/saml_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/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -39,10 +37,6 @@ func (a *SamlRequest) checkLoginClient(ctx context.Context, permissionCheck doma //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) }() @@ -62,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) { 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 6c6c2b6ebe..3a062ac5fd 100644 --- a/internal/query/saml_request_test.go +++ b/internal/query/saml_request_test.go @@ -22,7 +22,6 @@ import ( func TestQueries_SamlRequestByID(t *testing.T) { expQuery := regexp.QuoteMeta(fmt.Sprintf( samlRequestByIDQuery, - asOfSystemTime, )) cols := []string{ @@ -148,8 +147,7 @@ func TestQueries_SamlRequestByID(t *testing.T) { 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_test.go b/internal/query/system_features_test.go index e460d38cec..fcd0f812f5 100644 --- a/internal/query/system_features_test.go +++ b/internal/query/system_features_test.go @@ -45,23 +45,23 @@ 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]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemActionsEventType, true, )), @@ -97,23 +97,23 @@ func TestQueries_GetSystemFeatures(t *testing.T) { 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]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemActionsEventType, false, )), @@ -121,7 +121,7 @@ func TestQueries_GetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, )), @@ -157,23 +157,23 @@ func TestQueries_GetSystemFeatures(t *testing.T) { 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]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemActionsEventType, false, )), @@ -181,7 +181,7 @@ func TestQueries_GetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, )), 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 4ea167d004..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" @@ -432,7 +431,7 @@ func (q *Queries) GetHumanProfile(ctx context.Context, userID string, queries .. 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) } @@ -456,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) } @@ -480,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) } @@ -567,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) } @@ -622,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() @@ -650,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) @@ -709,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) } @@ -779,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 @@ -788,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 @@ -933,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 @@ -984,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(), @@ -1007,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) @@ -1057,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(), @@ -1068,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) @@ -1104,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(), @@ -1115,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) @@ -1151,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 @@ -1196,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 @@ -1303,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(), @@ -1312,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{} @@ -1340,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 @@ -1389,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 ab6c464bad..8b26389f1a 100644 --- a/internal/query/user_auth_method.go +++ b/internal/query/user_auth_method.go @@ -12,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" @@ -159,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") @@ -186,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(), @@ -366,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(), @@ -378,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) @@ -416,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 @@ -437,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) diff --git a/internal/query/user_auth_method_test.go b/internal/query/user_auth_method_test.go index 47c50c4505..03f2e2174a 100644 --- a/internal/query/user_auth_method_test.go +++ b/internal/query/user_auth_method_test.go @@ -190,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", @@ -214,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", @@ -231,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", @@ -248,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", @@ -416,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) } @@ -433,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) } @@ -465,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) } @@ -497,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) } @@ -529,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) } @@ -567,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) } @@ -605,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) } @@ -643,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) } @@ -666,7 +662,7 @@ func Test_UserAuthMethodPrepares(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_grant.go b/internal/query/user_grant.go index 265d8eaae1..c3f24c066e 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -246,7 +245,7 @@ func (q *Queries) UserGrant(ctx context.Context, shouldTriggerBulk bool, queries traceSpan.EndWithError(err) } - query, scan := prepareUserGrantQuery(ctx, q.client) + query, scan := prepareUserGrantQuery() for _, q := range queries { query = q.toQuery(query) } @@ -274,7 +273,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh traceSpan.EndWithError(err) } - query, scan := prepareUserGrantsQuery(ctx, q.client) + query, scan := prepareUserGrantsQuery() eq := sq.Eq{UserGrantInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -298,7 +297,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh return grants, nil } -func prepareUserGrantQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserGrant, error)) { +func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, error)) { return sq.Select( UserGrantID.identifier(), UserGrantCreationDate.identifier(), @@ -336,7 +335,7 @@ func prepareUserGrantQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu LeftJoin(join(OrgColumnID, UserGrantResourceOwner)). LeftJoin(join(ProjectColumnID, UserGrantProjectID)). LeftJoin(join(GrantedOrgColumnId, UserResourceOwnerCol)). - LeftJoin(join(LoginNameUserIDCol, UserGrantUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), @@ -421,7 +420,7 @@ func prepareUserGrantQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } } -func prepareUserGrantsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, error)) { +func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, error)) { return sq.Select( UserGrantID.identifier(), UserGrantCreationDate.identifier(), @@ -461,7 +460,7 @@ func prepareUserGrantsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB LeftJoin(join(OrgColumnID, UserGrantResourceOwner)). LeftJoin(join(ProjectColumnID, UserGrantProjectID)). LeftJoin(join(GrantedOrgColumnId, UserResourceOwnerCol)). - LeftJoin(join(LoginNameUserIDCol, UserGrantUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/user_grant_test.go b/internal/query/user_grant_test.go index 6cfa0b563b..6a640c2ef2 100644 --- a/internal/query/user_grant_test.go +++ b/internal/query/user_grant_test.go @@ -47,7 +47,6 @@ var ( " LEFT JOIN projections.projects4 ON projections.user_grants5.project_id = projections.projects4.id AND projections.user_grants5.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.users14.resource_owner = granted_orgs.id AND projections.users14.instance_id = granted_orgs.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants5.user_id = projections.login_names3.user_id AND projections.user_grants5.instance_id = projections.login_names3.instance_id" + - ` AS OF SYSTEM TIME '-1 ms' ` + " WHERE projections.login_names3.is_primary = $1") userGrantCols = []string{ "id", @@ -110,7 +109,6 @@ var ( " LEFT JOIN projections.projects4 ON projections.user_grants5.project_id = projections.projects4.id AND projections.user_grants5.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.users14.resource_owner = granted_orgs.id AND projections.users14.instance_id = granted_orgs.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants5.user_id = projections.login_names3.user_id AND projections.user_grants5.instance_id = projections.login_names3.instance_id" + - ` AS OF SYSTEM TIME '-1 ms' ` + " WHERE projections.login_names3.is_primary = $1") userGrantsCols = append( userGrantCols, @@ -1008,7 +1006,7 @@ func Test_UserGrantPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/user_membership.go b/internal/query/user_membership.go index 7ba2629cfa..cae2b4dae3 100644 --- a/internal/query/user_membership.go +++ b/internal/query/user_membership.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -138,7 +137,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer wg.Wait() } - query, queryArgs, scan := prepareMembershipsQuery(ctx, q.client, queries) + query, queryArgs, scan := prepareMembershipsQuery(queries) eq := sq.Eq{membershipInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -237,7 +236,7 @@ func getMembershipFromQuery(queries *MembershipSearchQuery) (string, []interface args } -func prepareMembershipsQuery(ctx context.Context, db prepareDatabase, queries *MembershipSearchQuery) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Memberships, error)) { +func prepareMembershipsQuery(queries *MembershipSearchQuery) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Memberships, error)) { query, args := getMembershipFromQuery(queries) return sq.Select( membershipUserID.identifier(), @@ -259,7 +258,7 @@ func prepareMembershipsQuery(ctx context.Context, db prepareDatabase, queries *M LeftJoin(join(ProjectColumnID, membershipProjectID)). LeftJoin(join(OrgColumnID, membershipOrgID)). LeftJoin(join(ProjectGrantColumnGrantID, membershipGrantID)). - LeftJoin(join(InstanceColumnID, membershipInstanceID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(InstanceColumnID, membershipInstanceID)). PlaceholderFormat(sq.Dollar), args, func(rows *sql.Rows) (*Memberships, error) { diff --git a/internal/query/user_membership_test.go b/internal/query/user_membership_test.go index a0ea3cda31..b0170182d1 100644 --- a/internal/query/user_membership_test.go +++ b/internal/query/user_membership_test.go @@ -1,7 +1,6 @@ package query import ( - "context" "database/sql" "database/sql/driver" "errors" @@ -87,8 +86,7 @@ var ( " LEFT JOIN projections.projects4 ON members.project_id = projections.projects4.id AND members.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.orgs1 ON members.org_id = projections.orgs1.id AND members.instance_id = projections.orgs1.instance_id" + " LEFT JOIN projections.project_grants4 ON members.grant_id = projections.project_grants4.grant_id AND members.instance_id = projections.project_grants4.instance_id" + - " LEFT JOIN projections.instances ON members.instance_id = projections.instances.id" + - ` AS OF SYSTEM TIME '-1 ms'`) + " LEFT JOIN projections.instances ON members.instance_id = projections.instances.id") membershipCols = []string{ "user_id", "roles", @@ -456,14 +454,14 @@ func Test_MembershipPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } -func prepareMembershipWrapper() func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { - return func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { - builder, _, fun := prepareMembershipsQuery(ctx, db, &MembershipSearchQuery{}) +func prepareMembershipWrapper() func() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { + return func() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { + builder, _, fun := prepareMembershipsQuery(&MembershipSearchQuery{}) return builder, fun } } diff --git a/internal/query/user_metadata.go b/internal/query/user_metadata.go index a3b7c1fd34..ff612f82c8 100644 --- a/internal/query/user_metadata.go +++ b/internal/query/user_metadata.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -87,7 +86,7 @@ func (q *Queries) GetUserMetadataByKey(ctx context.Context, shouldTriggerBulk bo traceSpan.EndWithError(err) } - query, scan := prepareUserMetadataQuery(ctx, q.client) + query, scan := prepareUserMetadataQuery() for _, q := range queries { query = q.toQuery(query) } @@ -119,7 +118,7 @@ func (q *Queries) SearchUserMetadataForUsers(ctx context.Context, shouldTriggerB traceSpan.EndWithError(err) } - query, scan := prepareUserMetadataListQuery(ctx, q.client) + query, scan := prepareUserMetadataListQuery() eq := sq.Eq{ UserMetadataUserIDCol.identifier(): userIDs, UserMetadataInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -151,7 +150,7 @@ func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool traceSpan.EndWithError(err) } - query, scan := prepareUserMetadataListQuery(ctx, q.client) + query, scan := prepareUserMetadataListQuery() eq := sq.Eq{ UserMetadataUserIDCol.identifier(): userID, UserMetadataInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -235,7 +234,7 @@ func NewUserMetadataExistsQuery(key string, value []byte, keyComparison TextComp ) } -func prepareUserMetadataQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserMetadata, error)) { +func prepareUserMetadataQuery() (sq.SelectBuilder, func(*sql.Row) (*UserMetadata, error)) { return sq.Select( UserMetadataCreationDateCol.identifier(), UserMetadataChangeDateCol.identifier(), @@ -244,7 +243,7 @@ func prepareUserMetadataQuery(ctx context.Context, db prepareDatabase) (sq.Selec UserMetadataKeyCol.identifier(), UserMetadataValueCol.identifier(), ). - From(userMetadataTable.identifier() + db.Timetravel(call.Took(ctx))). + From(userMetadataTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*UserMetadata, error) { m := new(UserMetadata) @@ -267,7 +266,7 @@ func prepareUserMetadataQuery(ctx context.Context, db prepareDatabase) (sq.Selec } } -func prepareUserMetadataListQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserMetadataList, error)) { +func prepareUserMetadataListQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserMetadataList, error)) { return sq.Select( UserMetadataCreationDateCol.identifier(), UserMetadataChangeDateCol.identifier(), @@ -277,7 +276,7 @@ func prepareUserMetadataListQuery(ctx context.Context, db prepareDatabase) (sq.S UserMetadataKeyCol.identifier(), UserMetadataValueCol.identifier(), countColumn.identifier()). - From(userMetadataTable.identifier() + db.Timetravel(call.Took(ctx))). + From(userMetadataTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*UserMetadataList, error) { metadata := make([]*UserMetadata, 0) diff --git a/internal/query/user_metadata_test.go b/internal/query/user_metadata_test.go index 7f9d1b8ed3..6236272da4 100644 --- a/internal/query/user_metadata_test.go +++ b/internal/query/user_metadata_test.go @@ -18,8 +18,7 @@ var ( ` projections.user_metadata5.sequence,` + ` projections.user_metadata5.key,` + ` projections.user_metadata5.value` + - ` FROM projections.user_metadata5` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.user_metadata5` userMetadataCols = []string{ "creation_date", "change_date", @@ -251,7 +250,7 @@ func Test_UserMetadataPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/user_password.go b/internal/query/user_password.go index ed77d0d3ae..1d0037f721 100644 --- a/internal/query/user_password.go +++ b/internal/query/user_password.go @@ -119,7 +119,6 @@ func (wm *HumanPasswordReadModel) Reduce() error { func (wm *HumanPasswordReadModel) Query() *eventstore.SearchQueryBuilder { query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). AwaitOpenTransactions(). - AllowTimeTravel(). AddQuery(). AggregateTypes(user.AggregateType). AggregateIDs(wm.AggregateID). diff --git a/internal/query/user_personal_access_token.go b/internal/query/user_personal_access_token.go index dadd635b6a..8ea33f51a4 100644 --- a/internal/query/user_personal_access_token.go +++ b/internal/query/user_personal_access_token.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -98,7 +97,7 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk traceSpan.EndWithError(err) } - query, scan := preparePersonalAccessTokenQuery(ctx, q.client) + query, scan := preparePersonalAccessTokenQuery() for _, q := range queries { query = q.toQuery(query) } @@ -128,7 +127,7 @@ func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *Perso ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := preparePersonalAccessTokensQuery(ctx, q.client) + query, scan := preparePersonalAccessTokensQuery() eq := sq.Eq{ PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -178,7 +177,7 @@ func (q *PersonalAccessTokenSearchQueries) toQuery(query sq.SelectBuilder) sq.Se return query } -func preparePersonalAccessTokenQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*PersonalAccessToken, error)) { +func preparePersonalAccessTokenQuery() (sq.SelectBuilder, func(*sql.Row) (*PersonalAccessToken, error)) { return sq.Select( PersonalAccessTokenColumnID.identifier(), PersonalAccessTokenColumnCreationDate.identifier(), @@ -188,7 +187,7 @@ func preparePersonalAccessTokenQuery(ctx context.Context, db prepareDatabase) (s PersonalAccessTokenColumnUserID.identifier(), PersonalAccessTokenColumnExpiration.identifier(), PersonalAccessTokenColumnScopes.identifier()). - From(personalAccessTokensTable.identifier() + db.Timetravel(call.Took(ctx))). + From(personalAccessTokensTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*PersonalAccessToken, error) { p := new(PersonalAccessToken) @@ -212,7 +211,7 @@ func preparePersonalAccessTokenQuery(ctx context.Context, db prepareDatabase) (s } } -func preparePersonalAccessTokensQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*PersonalAccessTokens, error)) { +func preparePersonalAccessTokensQuery() (sq.SelectBuilder, func(*sql.Rows) (*PersonalAccessTokens, error)) { return sq.Select( PersonalAccessTokenColumnID.identifier(), PersonalAccessTokenColumnCreationDate.identifier(), @@ -223,7 +222,7 @@ func preparePersonalAccessTokensQuery(ctx context.Context, db prepareDatabase) ( PersonalAccessTokenColumnExpiration.identifier(), PersonalAccessTokenColumnScopes.identifier(), countColumn.identifier()). - From(personalAccessTokensTable.identifier() + db.Timetravel(call.Took(ctx))). + From(personalAccessTokensTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*PersonalAccessTokens, error) { personalAccessTokens := make([]*PersonalAccessToken, 0) diff --git a/internal/query/user_personal_access_token_test.go b/internal/query/user_personal_access_token_test.go index 79ba700ed5..dd3ed37e62 100644 --- a/internal/query/user_personal_access_token_test.go +++ b/internal/query/user_personal_access_token_test.go @@ -23,8 +23,7 @@ var ( " projections.personal_access_tokens3.user_id," + " projections.personal_access_tokens3.expiration," + " projections.personal_access_tokens3.scopes" + - " FROM projections.personal_access_tokens3" + - ` AS OF SYSTEM TIME '-1 ms'`) + " FROM projections.personal_access_tokens3") personalAccessTokenCols = []string{ "id", "creation_date", @@ -45,8 +44,7 @@ var ( " projections.personal_access_tokens3.expiration," + " projections.personal_access_tokens3.scopes," + " COUNT(*) OVER ()" + - " FROM projections.personal_access_tokens3" + - " AS OF SYSTEM TIME '-1 ms'") + " FROM projections.personal_access_tokens3") personalAccessTokensCols = []string{ "id", "creation_date", @@ -266,7 +264,7 @@ func Test_PersonalAccessTokenPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/user_schema.go b/internal/query/user_schema.go index ff5117d264..d3ea4dee79 100644 --- a/internal/query/user_schema.go +++ b/internal/query/user_schema.go @@ -96,7 +96,7 @@ func (q *Queries) GetUserSchemaByID(ctx context.Context, id string) (userSchema } query, scan := prepareUserSchemaQuery() - return genericRowQuery[*UserSchema](ctx, q.client, query.Where(eq), scan) + return genericRowQuery(ctx, q.client, query.Where(eq), scan) } func (q *Queries) SearchUserSchema(ctx context.Context, queries *UserSchemaSearchQueries) (userSchemas *UserSchemas, err error) { @@ -108,7 +108,7 @@ func (q *Queries) SearchUserSchema(ctx context.Context, queries *UserSchemaSearc } query, scan := prepareUserSchemasQuery() - return genericRowsQueryWithState[*UserSchemas](ctx, q.client, userSchemaTable, combineToWhereStmt(query, queries.toQuery, eq), scan) + return genericRowsQueryWithState(ctx, q.client, userSchemaTable, combineToWhereStmt(query, queries.toQuery, eq), scan) } func (q *UserSchemaSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { diff --git a/internal/query/user_test.go b/internal/query/user_test.go index 16b08611f0..50d65cc1ec 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -6,7 +6,6 @@ import ( "database/sql/driver" "errors" "fmt" - "reflect" "regexp" "testing" @@ -268,8 +267,7 @@ var ( ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` userCols = []string{ "id", "creation_date", @@ -319,8 +317,7 @@ var ( ` projections.users14_humans.gender,` + ` projections.users14_humans.avatar_key` + ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` profileCols = []string{ "id", "creation_date", @@ -345,8 +342,7 @@ var ( ` projections.users14_humans.email,` + ` projections.users14_humans.is_email_verified` + ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` emailCols = []string{ "id", "creation_date", @@ -366,8 +362,7 @@ var ( ` projections.users14_humans.phone,` + ` projections.users14_humans.is_phone_verified` + ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` phoneCols = []string{ "id", "creation_date", @@ -385,8 +380,7 @@ var ( ` projections.users14_humans.email,` + ` projections.users14_humans.is_email_verified` + ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` userUniqueCols = []string{ "id", "state", @@ -428,8 +422,7 @@ var ( ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` notifyUserCols = []string{ "id", "creation_date", @@ -497,8 +490,7 @@ var ( ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` usersCols = []string{ "id", "creation_date", @@ -1572,12 +1564,7 @@ func Test_UserPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - params := defaultPrepareArgs - if reflect.TypeOf(tt.prepare).NumIn() == 0 { - params = []reflect.Value{} - } - - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, params...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/userinfo_test.go b/internal/query/userinfo_test.go index 6ded7b4eed..5314283635 100644 --- a/internal/query/userinfo_test.go +++ b/internal/query/userinfo_test.go @@ -429,8 +429,7 @@ func TestQueries_GetOIDCUserInfo(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") @@ -476,8 +475,7 @@ func TestQueries_GetOIDCUserinfoClientByID(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") diff --git a/internal/query/web_key_test.go b/internal/query/web_key_test.go index 6008ec6528..80d07bfa13 100644 --- a/internal/query/web_key_test.go +++ b/internal/query/web_key_test.go @@ -208,8 +208,7 @@ func TestQueries_GetActiveSigningWebKey(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, keyEncryptionAlgorithm: alg, } @@ -307,8 +306,7 @@ func TestQueries_ListWebKeys(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } got, err := q.ListWebKeys(ctx) @@ -369,8 +367,7 @@ func TestQueries_GetWebKeySet(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } got, err := q.GetWebKeySet(ctx) diff --git a/internal/queue/queue.go b/internal/queue/queue.go index b45a7eb8cb..d680221753 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -27,9 +27,6 @@ type Config struct { } func NewQueue(config *Config) (_ *Queue, err error) { - if config.Client.Type() == "cockroach" { - return nil, nil - } return &Queue{ driver: riverpgxv5.New(config.Client.Pool), config: &river.Config{ diff --git a/internal/repository/execution/queue.go b/internal/repository/execution/queue.go new file mode 100644 index 0000000000..28f8edbf31 --- /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 []byte `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/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/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/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/zitadel/action/v2beta/action_service.proto b/proto/zitadel/action/v2beta/action_service.proto new file mode 100644 index 0000000000..d1eebfa344 --- /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/includes called during the execution. + repeated ExecutionTargetType 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 87% rename from proto/zitadel/resources/action/v3alpha/execution.proto rename to proto/zitadel/action/v2beta/execution.proto index 375ab02b86..4f4ad1ce88 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,21 +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 { + 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/includes called during the execution. - repeated ExecutionTargetType targets = 1; -} - -message GetExecution { - zitadel.resources.object.v3alpha.Details details = 1; - Condition condition = 2; - Execution execution = 3; + repeated ExecutionTargetType targets = 4; } message ExecutionTargetType { diff --git a/proto/zitadel/resources/action/v3alpha/query.proto b/proto/zitadel/action/v2beta/query.proto similarity index 90% rename from proto/zitadel/resources/action/v3alpha/query.proto rename to proto/zitadel/action/v2beta/query.proto index fb51543085..564db8bc9f 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 { @@ -52,6 +53,18 @@ message IncludeFilter { ]; } +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 { oneof filter { option (validate.required) = true; @@ -71,7 +84,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 +110,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/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/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 d59d6e67ec..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; 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