From 257bef974a6d227e50142b3895b2e46cd42051f5 Mon Sep 17 00:00:00 2001 From: Stygmates Date: Thu, 24 Apr 2025 11:56:52 +0200 Subject: [PATCH 01/26] fix: text buttons overflow in login page (#9637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved The text of some of the buttons in the login page overflows in some languages ![image](https://github.com/user-attachments/assets/ef3d3bfe-8966-4be5-8d3b-3b0b72ce5e49) # How the Problems Are Solved Updated the css to set the overflow to hidden and text-overflow to ellipsis, this is the simplest fix I could come up with, if you have a better alternative feel free to tell me what you would prefer 🙏 ![image](https://github.com/user-attachments/assets/cdfa1f7b-535a-419d-ba9d-a57ec332d976) # Additional Changes None # Additional Context I couldn't test the following case locally since I had trouble setting up a SMTP provider locally, but the class affected by my change should also target this case, if someone could test it before merging it :pray:: ![315957139-6a630056-82b9-42cd-85a6-8819f2e1873b](https://github.com/user-attachments/assets/f6860db3-d6a0-4e4d-b9e6-0b1968145047) - Closes #7619 Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> --- .../resources/themes/scss/styles/button/button_base.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/api/ui/login/static/resources/themes/scss/styles/button/button_base.scss b/internal/api/ui/login/static/resources/themes/scss/styles/button/button_base.scss index dd53dceb79..aeeeba541c 100644 --- a/internal/api/ui/login/static/resources/themes/scss/styles/button/button_base.scss +++ b/internal/api/ui/login/static/resources/themes/scss/styles/button/button_base.scss @@ -39,7 +39,9 @@ $lgn-icon-button-line-height: 40px !default; padding: $lgn-button-padding; border-radius: $lgn-button-border-radius; - overflow: visible; + overflow: hidden; + text-overflow: ellipsis; + transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); &[disabled] { From 106e360c195f37400025b1549480654cb9e12268 Mon Sep 17 00:00:00 2001 From: Michael Sacher <144212572+msceex@users.noreply.github.com> Date: Fri, 25 Apr 2025 08:45:39 +0200 Subject: [PATCH 02/26] docs(adopters): Clean Energy Exchange AG (#9686) doc: ADOPTERS.md ceex # Which Problems Are Solved Replace this example text with a concise list of problems that this PR solves. For example: - If the property XY is not given, the system crashes with a nil pointer exception. # How the Problems Are Solved Replace this example text with a concise list of changes that this PR introduces. For example: - Validates if property XY is given and throws an error if not # Additional Changes Replace this example text with a concise list of additional changes that this PR introduces, that are not directly solving the initial problem but are related. For example: - The docs explicitly describe that the property XY is mandatory - Adds missing translations for validations. # Additional Context Replace this example with links to related issues, discussions, discord threads, or other sources with more context. Use the Closing #issue syntax for issues that are resolved with this PR. - Closes #xxx - Discussion #xxx - Follow-up for PR #xxx - https://discord.com/channels/xxx/xxx --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index 0573099cf9..876d984347 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -23,6 +23,7 @@ If you are using Zitadel, please consider adding yourself as a user with a quick | OpenAIP | [@openaip](https://github.com/openAIP) | Using Zitadel Cloud for everything related to user authentication. | | Smat.io | [@smatio](https://github.com/smatio) - [@lukasver](https://github.com/lukasver) | Zitadel for authentication in cloud applications while offering B2B portfolio management solutions for professional investors | | roclub GmbH | [@holgerson97](https://github.com/holgerson97) | Roclub builds a telehealth application to enable remote MRI/CT examinations. | +| CEEX AG | [@cleanenergyexchange](https://github.com/cleanenergyexchange) | Using Zitadel cloud for our SaaS products that support the sustainabel energy transistion | | Organization Name | contact@example.com | Description of how they use Zitadel | | Individual Name | contact@example.com | Description of how they use Zitadel | From 4ffd4ef38126c73377bae1ba0508a45366d814e6 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 25 Apr 2025 09:12:42 +0200 Subject: [PATCH 03/26] fix(actions): handle empty deny list correctly (#9753) # Which Problems Are Solved A customer reached out that after an upgrade, actions would always fail with the error "host is denied" when calling an external API. This is due to a security fix (https://github.com/zitadel/zitadel/security/advisories/GHSA-6cf5-w9h3-4rqv), where a DNS lookup was added to check whether the host name resolves to a denied IP or subnet. If the lookup fails due to the internal DNS setup, the action fails as well. Additionally, the lookup was also performed when the deny list was empty. # How the Problems Are Solved - Prevent DNS lookup when deny list is empty - Properly initiate deny list and prevent empty entries # Additional Changes - Log the reason for blocked address (domain, IP, subnet) # Additional Context - reported by a customer - needs backport to 2.70.x, 2.71.x and 3.0.0 rc --- cmd/start/config.go | 4 ++- internal/actions/http_module.go | 20 +++++++-------- internal/actions/http_module_config.go | 35 ++++++++++++++++++++++---- internal/actions/http_module_test.go | 31 ++++++++++++++++------- 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/cmd/start/config.go b/cmd/start/config.go index e973c40479..78b6f0afe0 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -128,7 +128,9 @@ func MustNewConfig(v *viper.Viper) *Config { logging.OnError(err).Fatal("unable to set profiler") id.Configure(config.Machine) - actions.SetHTTPConfig(&config.Actions.HTTP) + if config.Actions != nil { + actions.SetHTTPConfig(&config.Actions.HTTP) + } // Copy the global role permissions mappings to the instance until we allow instance-level configuration over the API. config.DefaultInstance.RolePermissionMappings = config.InternalAuthZ.RolePermissionMappings diff --git a/internal/actions/http_module.go b/internal/actions/http_module.go index 2f9d09932c..db7253428d 100644 --- a/internal/actions/http_module.go +++ b/internal/actions/http_module.go @@ -176,16 +176,16 @@ type transport struct { } func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { - if httpConfig == nil { + if httpConfig == nil || len(httpConfig.DenyList) == 0 { return http.DefaultTransport.RoundTrip(req) } - if t.isHostBlocked(httpConfig.DenyList, req.URL) { - return nil, zerrors.ThrowInvalidArgument(nil, "ACTIO-N72d0", "host is denied") + if err := t.isHostBlocked(httpConfig.DenyList, req.URL); err != nil { + return nil, zerrors.ThrowInvalidArgument(err, "ACTIO-N72d0", "host is denied") } return http.DefaultTransport.RoundTrip(req) } -func (t *transport) isHostBlocked(denyList []AddressChecker, address *url.URL) bool { +func (t *transport) isHostBlocked(denyList []AddressChecker, address *url.URL) error { host := address.Hostname() ip := net.ParseIP(host) ips := []net.IP{ip} @@ -194,17 +194,17 @@ func (t *transport) isHostBlocked(denyList []AddressChecker, address *url.URL) b var err error ips, err = t.lookup(host) if err != nil { - return true + return zerrors.ThrowInternal(err, "ACTIO-4m9s2", "lookup failed") } } - for _, blocked := range denyList { - if blocked.Matches(ips, host) { - return true + for _, denied := range denyList { + if err := denied.IsDenied(ips, host); err != nil { + return err } } - return false + return nil } type AddressChecker interface { - Matches([]net.IP, string) bool + IsDenied([]net.IP, string) error } diff --git a/internal/actions/http_module_config.go b/internal/actions/http_module_config.go index d1b965814e..eaab9e754e 100644 --- a/internal/actions/http_module_config.go +++ b/internal/actions/http_module_config.go @@ -1,6 +1,8 @@ package actions import ( + "errors" + "fmt" "net" "reflect" "strings" @@ -60,6 +62,9 @@ func HTTPConfigDecodeHook(from, to reflect.Value) (interface{}, error) { } func NewHostChecker(entry string) (AddressChecker, error) { + if entry == "" { + return nil, nil + } _, network, err := net.ParseCIDR(entry) if err == nil { return &HostChecker{Net: network}, nil @@ -76,19 +81,39 @@ type HostChecker struct { Domain string } -func (c *HostChecker) Matches(ips []net.IP, address string) bool { +type AddressDeniedError struct { + deniedBy string +} + +func NewAddressDeniedError(deniedBy string) *AddressDeniedError { + return &AddressDeniedError{deniedBy: deniedBy} +} + +func (e *AddressDeniedError) Error() string { + return fmt.Sprintf("address is denied by '%s'", e.deniedBy) +} + +func (e *AddressDeniedError) Is(target error) bool { + var addressDeniedErr *AddressDeniedError + if !errors.As(target, &addressDeniedErr) { + return false + } + return e.deniedBy == addressDeniedErr.deniedBy +} + +func (c *HostChecker) IsDenied(ips []net.IP, address string) error { // if the address matches the domain, no additional checks as needed if c.Domain == address { - return true + return NewAddressDeniedError(c.Domain) } // otherwise we need to check on ips (incl. the resolved ips of the host) for _, ip := range ips { if c.Net != nil && c.Net.Contains(ip) { - return true + return NewAddressDeniedError(c.Net.String()) } if c.IP != nil && c.IP.Equal(ip) { - return true + return NewAddressDeniedError(c.IP.String()) } } - return false + return nil } diff --git a/internal/actions/http_module_test.go b/internal/actions/http_module_test.go index 7a1f8d7816..50a007feeb 100644 --- a/internal/actions/http_module_test.go +++ b/internal/actions/http_module_test.go @@ -3,6 +3,7 @@ package actions import ( "bytes" "context" + "errors" "io" "net" "net/http" @@ -11,6 +12,7 @@ import ( "testing" "github.com/dop251/goja" + "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/logstore/record" @@ -34,21 +36,21 @@ func Test_isHostBlocked(t *testing.T) { name string fields fields args args - want bool + want error }{ { name: "in range", args: args{ address: mustNewURL(t, "https://192.168.5.4/hodor"), }, - want: true, + want: NewAddressDeniedError("192.168.5.0/24"), }, { name: "exact ip", args: args{ address: mustNewURL(t, "http://127.0.0.1:8080/hodor"), }, - want: true, + want: NewAddressDeniedError("127.0.0.1"), }, { name: "address match", @@ -60,7 +62,7 @@ func Test_isHostBlocked(t *testing.T) { args: args{ address: mustNewURL(t, "https://test.com:42/hodor"), }, - want: true, + want: NewAddressDeniedError("test.com"), }, { name: "address not match", @@ -72,7 +74,7 @@ func Test_isHostBlocked(t *testing.T) { args: args{ address: mustNewURL(t, "https://test2.com/hodor"), }, - want: false, + want: nil, }, { name: "looked up ip matches", @@ -84,7 +86,19 @@ func Test_isHostBlocked(t *testing.T) { args: args{ address: mustNewURL(t, "https://test2.com/hodor"), }, - want: true, + want: NewAddressDeniedError("127.0.0.1"), + }, + { + name: "looked up failure", + fields: fields{ + lookup: func(host string) ([]net.IP, error) { + return nil, errors.New("some error") + }, + }, + args: args{ + address: mustNewURL(t, "https://test2.com/hodor"), + }, + want: zerrors.ThrowInternal(nil, "ACTIO-4m9s2", "lookup failed"), }, } for _, tt := range tests { @@ -92,9 +106,8 @@ func Test_isHostBlocked(t *testing.T) { trans := &transport{ lookup: tt.fields.lookup, } - if got := trans.isHostBlocked(denyList, tt.args.address); got != tt.want { - t.Errorf("isHostBlocked() = %v, want %v", got, tt.want) - } + got := trans.isHostBlocked(denyList, tt.args.address) + assert.ErrorIs(t, got, tt.want) }) } } From 65bb559bbec05cdb98539f6583766d653c916145 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Fri, 25 Apr 2025 14:04:29 +0200 Subject: [PATCH 04/26] docs(API_DESIGN.md): adding guidlines around API returns when multiple resources created (#9797) # Which Problems Are Solved Updating API_Design.md to include guidelines to specify all created resources created from an API call # How the Problems Are Solved This makes things clearer to the user if everything requested was actually created and helps with testing. See https://github.com/zitadel/zitadel/pull/9352 # Additional Context - Related https://github.com/zitadel/zitadel/issues/6305 - Related https://github.com/zitadel/zitadel/pull/9352 --------- Co-authored-by: Livio Spring --- API_DESIGN.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/API_DESIGN.md b/API_DESIGN.md index 7df13d6588..ea37df5a24 100644 --- a/API_DESIGN.md +++ b/API_DESIGN.md @@ -56,6 +56,10 @@ Provide clear and concise documentation for the API. Do not rely on implicit fallbacks or defaults if the client does not provide certain parameters. Only use defaults if they are explicitly documented, such as returning a result set for the whole instance if no filter is provided. +Some API calls may create multiple resources such as in the case of `zitadel.org.v2.AddOrganization`, where you can create an organization AND multiple users as admin. +In such cases the response should include **ALL** created resources and their ids. This removes any ambiguity from the users perspective whether or not +the additional resources were created and it also helps in testing. + ### Naming Conventions Names of resources, fields and methods MUST be descriptive and consistent. @@ -371,4 +375,4 @@ message VerifyEmailRequest{ ]; } -``` \ No newline at end of file +``` From 84628671bd207a6806667cd266b87cb4771da1fa Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Mon, 28 Apr 2025 11:02:33 +0200 Subject: [PATCH 05/26] chore: only download release relevant artifacts (#9808) # Which Problems Are Solved https://github.com/zitadel/zitadel/pull/9765 fixed an issue for with actions cache service. The PR updated the push action, which now also provides a build summary. The "release" step tries to download all artifacts, which now fails: https://github.com/zitadel/zitadel/actions/runs/14660464768/job/41145285454 # How the Problems Are Solved Only download relevant artifacts, which are published as part of the release. # Additional Changes None # Additional Context None --- .github/workflows/version.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index cf11e944f8..063f6956a5 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -32,6 +32,7 @@ jobs: if: ${{ !inputs.dry_run }} with: path: .artifacts + pattern: "{checksums.txt,zitadel-*}" - name: Semantic Release uses: cycjimmy/semantic-release-action@v4 From b8ba7bd5badd8b9ee43316465ae5748e6fab0f50 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Mon, 28 Apr 2025 11:24:50 +0200 Subject: [PATCH 06/26] fix: remove action feature flag and include execution (#9727) # Which Problems Are Solved Actions v2 is not a feature flag anymore, include functionality on executions is not used and json tags of proto messages are handled incorrectly. # How the Problems Are Solved - Remove actions from the feature flags on system and instance level - Remove include type on executions, only in the API, later maybe in the handling logic as well - Use protojson in request and response handling of actions v2 # Additional Changes - Correct integration tests for request and response handling - Use json.RawMessage for events, so that the event payload is not base64 encoded - Added separate context for async webhook calls, that executions are not cancelled when called async # Additional Context Related to #9759 Closes #9710 --------- Co-authored-by: Livio Spring --- .../guides/integrate/actions/testing-event.md | 9 +- .../actions/testing-request-manipulation.md | 25 ++- .../actions/testing-request-signature.md | 5 + .../integrate/actions/testing-request.md | 5 + .../actions/testing-response-manipulation.md | 48 ++++- .../integrate/actions/testing-response.md | 5 + docs/docs/guides/integrate/actions/usage.md | 12 +- internal/api/grpc/action/v2beta/execution.go | 54 +----- .../integration_test/execution_target_test.go | 99 +++++----- .../v2beta/integration_test/execution_test.go | 172 +++--------------- .../v2beta/integration_test/query_test.go | 145 ++++++++------- .../v2beta/integration_test/server_test.go | 46 ----- .../v2beta/integration_test/target_test.go | 3 - internal/api/grpc/action/v2beta/query.go | 24 +-- internal/api/grpc/action/v2beta/server.go | 10 - internal/api/grpc/action/v2beta/target.go | 9 - internal/api/grpc/feature/v2/converter.go | 4 - .../api/grpc/feature/v2/converter_test.go | 20 -- .../v2/integration_test/feature_test.go | 14 -- internal/api/grpc/feature/v2beta/converter.go | 4 - .../api/grpc/feature/v2beta/converter_test.go | 20 -- .../v2beta/integration_test/feature_test.go | 13 -- .../middleware/execution_interceptor.go | 62 ++++--- .../middleware/execution_interceptor_test.go | 32 ++-- internal/command/instance_features.go | 2 - internal/command/instance_features_model.go | 5 - internal/command/instance_features_test.go | 23 --- internal/command/system_features.go | 2 - internal/command/system_features_model.go | 5 - internal/command/system_features_test.go | 28 --- internal/execution/execution.go | 6 +- internal/execution/execution_test.go | 43 ++--- internal/feature/feature.go | 3 +- internal/feature/key_enumer.go | 8 +- internal/integration/action.go | 61 +++++++ internal/integration/client.go | 2 +- internal/query/instance_features.go | 1 - internal/query/instance_features_model.go | 7 +- internal/query/instance_features_test.go | 24 --- .../query/projection/instance_features.go | 4 - internal/query/projection/system_features.go | 4 - internal/query/system_features.go | 1 - internal/query/system_features_model.go | 6 +- internal/query/system_features_test.go | 24 --- internal/repository/execution/queue.go | 20 +- .../feature/feature_v2/eventstore.go | 2 - .../repository/feature/feature_v2/feature.go | 2 - proto/buf.yaml | 4 + .../action/v2beta/action_service.proto | 4 +- proto/zitadel/action/v2beta/execution.proto | 14 +- proto/zitadel/action/v2beta/query.proto | 11 -- proto/zitadel/feature/v2/instance.proto | 17 +- proto/zitadel/feature/v2/system.proto | 18 +- proto/zitadel/feature/v2beta/instance.proto | 17 +- proto/zitadel/feature/v2beta/system.proto | 18 +- 55 files changed, 427 insertions(+), 799 deletions(-) diff --git a/docs/docs/guides/integrate/actions/testing-event.md b/docs/docs/guides/integrate/actions/testing-event.md index 69a33f6d3e..8b4502703b 100644 --- a/docs/docs/guides/integrate/actions/testing-event.md +++ b/docs/docs/guides/integrate/actions/testing-event.md @@ -145,7 +145,14 @@ the [Sent information Event](./usage#sent-information-event) payload description "event_type": "user.human.added", "created_at": "2025-03-27T10:22:43.262665+01:00", "userID": "312909075212468632", - "event_payload": "eyJ1c2VyTmFtZSI6ImV4YW1wbGVAdGVzdC5jb20iLCJmaXJzdE5hbWUiOiJUZXN0IiwibGFzdE5hbWUiOiJVc2VyIiwiZGlzcGxheU5hbWUiOiJUZXN0IFVzZXIiLCJwcmVmZXJyZWRMYW5ndWFnZSI6InVuZCIsImVtYWlsIjoiZXhhbXBsZUB0ZXN0LmNvbSJ9" + "event_payload": { + "userName":"example@test.com", + "firstName":"Test", + "lastName":"User", + "displayName":"Test User", + "preferredLanguage":"und", + "email":"example@test.com" + } } ``` diff --git a/docs/docs/guides/integrate/actions/testing-request-manipulation.md b/docs/docs/guides/integrate/actions/testing-request-manipulation.md index f727b56144..1cb4f1776a 100644 --- a/docs/docs/guides/integrate/actions/testing-request-manipulation.md +++ b/docs/docs/guides/integrate/actions/testing-request-manipulation.md @@ -18,6 +18,11 @@ Note that this guide assumes that ZITADEL is running on the same machine as the In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. ::: +:::warning +To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request is a protocol buffer message, to avoid potential problems with the attribute names. +::: + ## Start example target To test the actions feature, you need to create a target that will be called when an API endpoint is called. @@ -37,10 +42,28 @@ import ( "net/http" "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "google.golang.org/protobuf/encoding/protojson" ) type contextRequest struct { - Request *user.AddHumanUserRequest `json:"request"` + Request *addHumanUserRequestWrapper `json:"request"` +} + +// addHumanUserRequestWrapper necessary to marshal and unmarshal the JSON into the proto message correctly +type addHumanUserRequestWrapper struct { + user.AddHumanUserRequest +} + +func (r *addHumanUserRequestWrapper) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *addHumanUserRequestWrapper) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r) } // call HandleFunc to read the request body, manipulate the content and return the manipulated request diff --git a/docs/docs/guides/integrate/actions/testing-request-signature.md b/docs/docs/guides/integrate/actions/testing-request-signature.md index 4565328e63..c1932a7d5b 100644 --- a/docs/docs/guides/integrate/actions/testing-request-signature.md +++ b/docs/docs/guides/integrate/actions/testing-request-signature.md @@ -18,6 +18,11 @@ Note that this guide assumes that ZITADEL is running on the same machine as the In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. ::: +:::warning +To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request is a protocol buffer message, to avoid potential problems with the attribute names. +::: + ## Start example target To test the actions feature, you need to create a target that will be called when an API endpoint is called. diff --git a/docs/docs/guides/integrate/actions/testing-request.md b/docs/docs/guides/integrate/actions/testing-request.md index e97b0ef25f..b2413e606e 100644 --- a/docs/docs/guides/integrate/actions/testing-request.md +++ b/docs/docs/guides/integrate/actions/testing-request.md @@ -18,6 +18,11 @@ Note that this guide assumes that ZITADEL is running on the same machine as the In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. ::: +:::warning +To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request is a protocol buffer message, to avoid potential problems with the attribute names. +::: + ## Start example target To test the actions feature, you need to create a target that will be called when an API endpoint is called. diff --git a/docs/docs/guides/integrate/actions/testing-response-manipulation.md b/docs/docs/guides/integrate/actions/testing-response-manipulation.md index cc10b8252a..9d95479b05 100644 --- a/docs/docs/guides/integrate/actions/testing-response-manipulation.md +++ b/docs/docs/guides/integrate/actions/testing-response-manipulation.md @@ -18,6 +18,11 @@ Note that this guide assumes that ZITADEL is running on the same machine as the In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. ::: +:::warning +To marshal and unmarshal the request and response please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request and response are protocol buffer messages, to avoid potential problems with the attribute names. +::: + ## Start example target To test the actions feature, you need to create a target that will be called when an API endpoint is called. @@ -37,11 +42,46 @@ import ( "net/http" "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "google.golang.org/protobuf/encoding/protojson" ) -type response struct { - Request *user.RetrieveIdentityProviderIntentRequest `json:"request"` - Response *user.RetrieveIdentityProviderIntentResponse `json:"response"` +type contextResponse struct { + Request *retrieveIdentityProviderIntentRequestWrapper `json:"request"` + Response *retrieveIdentityProviderIntentResponseWrapper `json:"response"` +} + +// RetrieveIdentityProviderIntentRequestWrapper necessary to marshal and unmarshal the JSON into the proto message correctly +type retrieveIdentityProviderIntentRequestWrapper struct { + user.RetrieveIdentityProviderIntentRequest +} + +func (r *retrieveIdentityProviderIntentRequestWrapper) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *retrieveIdentityProviderIntentRequestWrapper) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r) +} + +// RetrieveIdentityProviderIntentResponseWrapper necessary to marshal and unmarshal the JSON into the proto message correctly +type retrieveIdentityProviderIntentResponseWrapper struct { + user.RetrieveIdentityProviderIntentResponse +} + +func (r *retrieveIdentityProviderIntentResponseWrapper) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *retrieveIdentityProviderIntentResponseWrapper) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r) } // call HandleFunc to read the response body, manipulate the content and return the response @@ -56,7 +96,7 @@ func call(w http.ResponseWriter, req *http.Request) { defer req.Body.Close() // read the response into the expected structure - request := new(response) + request := new(contextResponse) if err := json.Unmarshal(sentBody, request); err != nil { http.Error(w, "error", http.StatusInternalServerError) } diff --git a/docs/docs/guides/integrate/actions/testing-response.md b/docs/docs/guides/integrate/actions/testing-response.md index 3eb824e95b..a2ab736505 100644 --- a/docs/docs/guides/integrate/actions/testing-response.md +++ b/docs/docs/guides/integrate/actions/testing-response.md @@ -18,6 +18,11 @@ Note that this guide assumes that ZITADEL is running on the same machine as the In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. ::: +:::warning +To marshal and unmarshal the request and response please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request and response are protocol buffer messages, to avoid potential problems with the attribute names. +::: + ## Start example target To test the actions feature, you need to create a target that will be called when an API endpoint is called. diff --git a/docs/docs/guides/integrate/actions/usage.md b/docs/docs/guides/integrate/actions/usage.md index 8a639f3c27..ba512ae549 100644 --- a/docs/docs/guides/integrate/actions/usage.md +++ b/docs/docs/guides/integrate/actions/usage.md @@ -36,6 +36,11 @@ The information sent to the Endpoint is structured as JSON: } ``` +:::warning +To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request is a protocol buffer message, to avoid potential problems with the attribute names. +::: + ### Sent information Response The information sent to the Endpoint is structured as JSON: @@ -56,6 +61,11 @@ The information sent to the Endpoint is structured as JSON: } ``` +:::warning +To marshal and unmarshal the request and response please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request and response are protocol buffer messages, to avoid potential problems with the attribute names. +::: + ### Sent information Function Information sent and expected back are specific to the function. @@ -338,7 +348,7 @@ The information sent to the Endpoint is structured as JSON: "event_type": "Type of the event", "created_at": "Time the event was created", "userID": "ID of the creator of the event", - "event_payload": "Base64 encoded content of the event" + "event_payload": "Content of the event in JSON format" } ``` diff --git a/internal/api/grpc/action/v2beta/execution.go b/internal/api/grpc/action/v2beta/execution.go index 8a7cd18ab4..5477a8128e 100644 --- a/internal/api/grpc/action/v2beta/execution.go +++ b/internal/api/grpc/action/v2beta/execution.go @@ -14,22 +14,10 @@ import ( ) func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionRequest) (*action.SetExecutionResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } reqTargets := req.GetTargets() targets := make([]*execution.Target, len(reqTargets)) for i, target := range reqTargets { - switch t := target.GetType().(type) { - case *action.ExecutionTargetType_Include: - include, err := conditionToInclude(t.Include) - if err != nil { - return nil, err - } - targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeInclude, Target: include} - case *action.ExecutionTargetType_Target: - targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: t.Target} - } + targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: target} } set := &command.SetExecution{ Targets: targets, @@ -60,59 +48,19 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque }, nil } -func conditionToInclude(cond *action.Condition) (string, error) { - switch t := cond.GetConditionType().(type) { - case *action.Condition_Request: - cond := executionConditionFromRequest(t.Request) - if err := cond.IsValid(); err != nil { - return "", err - } - return cond.ID(domain.ExecutionTypeRequest), nil - case *action.Condition_Response: - cond := executionConditionFromResponse(t.Response) - if err := cond.IsValid(); err != nil { - return "", err - } - return cond.ID(domain.ExecutionTypeRequest), nil - case *action.Condition_Event: - cond := executionConditionFromEvent(t.Event) - if err := cond.IsValid(); err != nil { - return "", err - } - return cond.ID(), nil - case *action.Condition_Function: - cond := command.ExecutionFunctionCondition(t.Function.GetName()) - if err := cond.IsValid(); err != nil { - return "", err - } - return cond.ID(), nil - default: - return "", zerrors.ThrowInvalidArgument(nil, "ACTION-9BBob", "Errors.Execution.ConditionInvalid") - } -} - func (s *Server) ListExecutionFunctions(ctx context.Context, _ *action.ListExecutionFunctionsRequest) (*action.ListExecutionFunctionsResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } return &action.ListExecutionFunctionsResponse{ Functions: s.ListActionFunctions(), }, nil } func (s *Server) ListExecutionMethods(ctx context.Context, _ *action.ListExecutionMethodsRequest) (*action.ListExecutionMethodsResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } return &action.ListExecutionMethodsResponse{ Methods: s.ListGRPCMethods(), }, nil } func (s *Server) ListExecutionServices(ctx context.Context, _ *action.ListExecutionServicesRequest) (*action.ListExecutionServicesResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } return &action.ListExecutionServicesResponse{ Services: s.ListGRPCServices(), }, nil diff --git a/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go index 6e3ab76fac..0c5018dbb6 100644 --- a/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go @@ -48,7 +48,6 @@ var ( func TestServer_ExecutionTarget(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) fullMethod := action.ActionService_GetTarget_FullMethodName @@ -77,14 +76,14 @@ func TestServer_ExecutionTarget(t *testing.T) { targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) // request received by target - wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request} + wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: middleware.Message{Message: request}} changedRequest := &action.GetTargetRequest{Id: targetCreated.GetId()} // replace original request with different targetID - urlRequest, closeRequest, calledRequest, _ := integration.TestServerCall(wantRequest, 0, http.StatusOK, changedRequest) + urlRequest, closeRequest, calledRequest, _ := integration.TestServerCallProto(wantRequest, 0, http.StatusOK, changedRequest) targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, false) - waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId())) + waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), []string{targetRequest.GetId()}) // expected response from the GetTarget expectedResponse := &action.GetTargetResponse{ @@ -103,9 +102,26 @@ func TestServer_ExecutionTarget(t *testing.T) { SigningKey: targetCreated.GetSigningKey(), }, } - // has to be set separately because of the pointers + + changedResponse := &action.GetTargetResponse{ + Target: &action.Target{ + Id: "changed", + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + }, + } + // content for update response.Target = &action.Target{ - Id: targetCreated.GetId(), + Id: "changed", CreationDate: targetCreated.GetCreationDate(), ChangeDate: targetCreated.GetCreationDate(), Name: targetCreatedName, @@ -119,18 +135,6 @@ func TestServer_ExecutionTarget(t *testing.T) { SigningKey: targetCreated.GetSigningKey(), } - // content for partial update - changedResponse := &action.GetTargetResponse{ - Target: &action.Target{ - Id: targetCreated.GetId(), - TargetType: &action.Target_RestCall{ - RestCall: &action.RESTCall{ - InterruptOnError: false, - }, - }, - }, - } - // response received by target wantResponse := &middleware.ContextInfoResponse{ FullMethod: fullMethod, @@ -138,14 +142,14 @@ func TestServer_ExecutionTarget(t *testing.T) { OrgID: orgID, ProjectID: projectID, UserID: userID, - Request: changedRequest, - Response: expectedResponse, + Request: middleware.Message{Message: changedRequest}, + Response: middleware.Message{Message: expectedResponse}, } // after request with different targetID, return changed response - targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCall(wantResponse, 0, http.StatusOK, changedResponse) + targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCallProto(wantResponse, 0, http.StatusOK, changedResponse) targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, false) - waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId())) + waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), []string{targetResponse.GetId()}) return func() { closeRequest() closeResponse() @@ -167,9 +171,7 @@ func TestServer_ExecutionTarget(t *testing.T) { Id: "something", }, want: &action.GetTargetResponse{ - Target: &action.Target{ - Id: "changed", - }, + // defined in the dependency function }, }, { @@ -181,11 +183,11 @@ func TestServer_ExecutionTarget(t *testing.T) { 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, calledRequest, _ := integration.TestServerCall(wantRequest, 0, http.StatusInternalServerError, &action.GetTargetRequest{Id: "notchanged"}) + wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: middleware.Message{Message: request}} + urlRequest, closeRequest, calledRequest, _ := integration.TestServerCallProto(wantRequest, 0, http.StatusInternalServerError, nil) targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, true) - waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId())) + waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), []string{targetRequest.GetId()}) // GetTarget with used target request.Id = targetRequest.GetId() return func() { @@ -234,17 +236,6 @@ func TestServer_ExecutionTarget(t *testing.T) { SigningKey: targetCreated.GetSigningKey(), }, } - // content for partial update - changedResponse := &action.GetTargetResponse{ - Target: &action.Target{ - Id: "changed", - TargetType: &action.Target_RestCall{ - RestCall: &action.RESTCall{ - InterruptOnError: false, - }, - }, - }, - } // response received by target wantResponse := &middleware.ContextInfoResponse{ @@ -253,14 +244,14 @@ func TestServer_ExecutionTarget(t *testing.T) { OrgID: orgID, ProjectID: projectID, UserID: userID, - Request: request, - Response: expectedResponse, + Request: middleware.Message{Message: request}, + Response: middleware.Message{Message: expectedResponse}, } // after request with different targetID, return changed response - targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCall(wantResponse, 0, http.StatusInternalServerError, changedResponse) + targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCallProto(wantResponse, 0, http.StatusInternalServerError, nil) targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, true) - waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId())) + waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), []string{targetResponse.GetId()}) return func() { closeResponse() }, func() bool { @@ -301,7 +292,6 @@ func TestServer_ExecutionTarget(t *testing.T) { func TestServer_ExecutionTarget_Event(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) event := "session.added" @@ -309,7 +299,7 @@ func TestServer_ExecutionTarget_Event(t *testing.T) { defer closeF() targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true) - waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), executionTargetsSingleTarget(targetResponse.GetId())) + waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()}) tests := []struct { name string @@ -359,7 +349,6 @@ func TestServer_ExecutionTarget_Event(t *testing.T) { func TestServer_ExecutionTarget_Event_LongerThanTargetTimeout(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) event := "session.added" @@ -368,7 +357,7 @@ func TestServer_ExecutionTarget_Event_LongerThanTargetTimeout(t *testing.T) { defer closeF() targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true) - waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), executionTargetsSingleTarget(targetResponse.GetId())) + waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()}) tests := []struct { name string @@ -412,7 +401,6 @@ func TestServer_ExecutionTarget_Event_LongerThanTargetTimeout(t *testing.T) { func TestServer_ExecutionTarget_Event_LongerThanTransactionTimeout(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) event := "session.added" @@ -420,7 +408,7 @@ func TestServer_ExecutionTarget_Event_LongerThanTransactionTimeout(t *testing.T) defer closeF() targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true) - waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), executionTargetsSingleTarget(targetResponse.GetId())) + waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()}) tests := []struct { name string @@ -474,7 +462,7 @@ func TestServer_ExecutionTarget_Event_LongerThanTransactionTimeout(t *testing.T) } } -func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *integration.Instance, condition *action.Condition, targets []*action.ExecutionTargetType) { +func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *integration.Instance, condition *action.Condition, targets []string) { instance.SetExecution(ctx, t, condition, targets) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) @@ -496,7 +484,7 @@ func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *in // always first check length, otherwise its failed anyway if assert.Len(ttt, gotTargets, len(targets)) { for i := range targets { - assert.EqualExportedValues(ttt, targets[i].GetType(), gotTargets[i].GetType()) + assert.EqualExportedValues(ttt, targets[i], gotTargets[i]) } } }, retryDuration, tick, "timeout waiting for expected execution result") @@ -589,7 +577,6 @@ func conditionFunction(function string) *action.Condition { func TestServer_ExecutionTargetPreUserinfo(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) @@ -785,7 +772,7 @@ func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *int 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.GetId())) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), []string{targetResp.GetId()}) return userResp.GetUserId(), closeF } @@ -903,7 +890,6 @@ func contextInfoForUserOIDC(instance *integration.Instance, function string, use func TestServer_ExecutionTargetPreAccessToken(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) @@ -1091,13 +1077,12 @@ func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance * 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.GetId())) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preaccesstoken"), []string{targetResp.GetId()}) return userResp.GetUserId(), closeF } func TestServer_ExecutionTargetPreSAMLResponse(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) @@ -1257,7 +1242,7 @@ func expectPreSAMLResponseExecution(ctx context.Context, t *testing.T, instance 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.GetId())) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("presamlresponse"), []string{targetResp.GetId()}) return userResp.GetUserId(), closeF } diff --git a/internal/api/grpc/action/v2beta/integration_test/execution_test.go b/internal/api/grpc/action/v2beta/integration_test/execution_test.go index 3af419d97b..2199b9f454 100644 --- a/internal/api/grpc/action/v2beta/integration_test/execution_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/execution_test.go @@ -15,17 +15,8 @@ import ( action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) -func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType { - return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Target{Target: id}}} -} - -func executionTargetsSingleInclude(include *action.Condition) []*action.ExecutionTargetType { - return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Include{Include: include}}} -} - func TestServer_SetExecution_Request(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) @@ -59,7 +50,7 @@ func TestServer_SetExecution_Request(t *testing.T) { Request: &action.RequestExecution{}, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantErr: true, }, @@ -76,7 +67,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantErr: true, }, @@ -93,7 +84,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantSetDate: true, }, @@ -110,7 +101,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantErr: true, }, @@ -127,7 +118,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantSetDate: true, }, @@ -144,7 +135,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantSetDate: true, }, @@ -181,125 +172,8 @@ func assertSetExecutionResponse(t *testing.T, creationDate, setDate time.Time, e } } -func TestServer_SetExecution_Request_Include(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - executionCond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{ - All: true, - }, - }, - }, - } - instance.SetExecution(isolatedIAMOwnerCTX, t, - executionCond, - executionTargetsSingleTarget(targetResp.GetId()), - ) - - circularExecutionService := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", - }, - }, - }, - } - instance.SetExecution(isolatedIAMOwnerCTX, t, - circularExecutionService, - executionTargetsSingleInclude(executionCond), - ) - circularExecutionMethod := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", - }, - }, - }, - } - instance.SetExecution(isolatedIAMOwnerCTX, t, - circularExecutionMethod, - executionTargetsSingleInclude(circularExecutionService), - ) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - wantSetDate bool - wantErr bool - }{ - { - name: "method, circular error", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: circularExecutionService, - Targets: executionTargetsSingleInclude(circularExecutionMethod), - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", - }, - }, - }, - }, - Targets: executionTargetsSingleInclude(executionCond), - }, - wantSetDate: true, - }, - { - name: "service, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.user.v2beta.UserService", - }, - }, - }, - }, - Targets: executionTargetsSingleInclude(executionCond), - }, - wantSetDate: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - creationDate := time.Now().UTC() - got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req) - setDate := time.Now().UTC() - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) - - // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - func TestServer_SetExecution_Response(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) @@ -333,7 +207,7 @@ func TestServer_SetExecution_Response(t *testing.T) { Response: &action.ResponseExecution{}, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantErr: true, }, @@ -350,7 +224,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantErr: true, }, @@ -367,7 +241,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantSetDate: true, }, @@ -384,7 +258,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantErr: true, }, @@ -401,7 +275,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantSetDate: true, }, @@ -418,7 +292,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantSetDate: true, }, @@ -444,7 +318,6 @@ func TestServer_SetExecution_Response(t *testing.T) { func TestServer_SetExecution_Event(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) @@ -480,7 +353,7 @@ func TestServer_SetExecution_Event(t *testing.T) { Event: &action.EventExecution{}, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantErr: true, }, @@ -497,7 +370,7 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantErr: true, }, @@ -514,7 +387,7 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantSetDate: true, }, @@ -531,7 +404,7 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantErr: true, }, @@ -548,7 +421,7 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantSetDate: true, }, @@ -565,7 +438,7 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantSetDate: true, }, @@ -582,7 +455,7 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantSetDate: true, }, @@ -608,7 +481,6 @@ func TestServer_SetExecution_Event(t *testing.T) { func TestServer_SetExecution_Function(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) @@ -642,7 +514,7 @@ func TestServer_SetExecution_Function(t *testing.T) { Response: &action.ResponseExecution{}, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantErr: true, }, @@ -655,7 +527,7 @@ func TestServer_SetExecution_Function(t *testing.T) { Function: &action.FunctionExecution{Name: "xxx"}, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantErr: true, }, @@ -668,7 +540,7 @@ func TestServer_SetExecution_Function(t *testing.T) { Function: &action.FunctionExecution{Name: "presamlresponse"}, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, wantSetDate: true, }, diff --git a/internal/api/grpc/action/v2beta/integration_test/query_test.go b/internal/api/grpc/action/v2beta/integration_test/query_test.go index c5159d39da..5c59bee5d1 100644 --- a/internal/api/grpc/action/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/query_test.go @@ -8,6 +8,7 @@ import ( "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" @@ -20,7 +21,6 @@ import ( func TestServer_GetTarget(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) type args struct { ctx context.Context @@ -213,7 +213,6 @@ func TestServer_GetTarget(t *testing.T) { func TestServer_ListTargets(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) type args struct { ctx context.Context @@ -446,7 +445,6 @@ func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationRes func TestServer_ListExecutions(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) @@ -475,7 +473,7 @@ func TestServer_ListExecutions(t *testing.T) { ctx: isolatedIAMOwnerCTX, dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0] - resp := instance.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetId())) + resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) // Set expected response with used values for SetExecution response.Result[0].CreationDate = resp.GetSetDate() @@ -516,7 +514,7 @@ func TestServer_ListExecutions(t *testing.T) { }, }, }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), + Targets: []string{targetResp.GetId()}, }, }, }, @@ -544,13 +542,12 @@ func TestServer_ListExecutions(t *testing.T) { }, }, } - targets := executionTargetsSingleTarget(target.GetId()) - resp := instance.SetExecution(ctx, t, cond, targets) + resp := instance.SetExecution(ctx, t, cond, []string{target.GetId()}) response.Result[0].CreationDate = resp.GetSetDate() response.Result[0].ChangeDate = resp.GetSetDate() response.Result[0].Condition = cond - response.Result[0].Targets = targets + response.Result[0].Targets = []string{target.GetId()} }, req: &action.ListExecutionsRequest{ Filters: []*action.ExecutionSearchFilter{{}}, @@ -564,63 +561,10 @@ func TestServer_ListExecutions(t *testing.T) { Result: []*action.Execution{ { Condition: &action.Condition{}, - Targets: executionTargetsSingleTarget(""), + Targets: []string{""}, }, }, }, - }, { - name: "list request single include", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { - cond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.management.v1.ManagementService/GetAction", - }, - }, - }, - } - instance.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetId())) - request.Filters[0].GetIncludeFilter().Include = cond - - includeCond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.management.v1.ManagementService/ListActions", - }, - }, - }, - } - includeTargets := executionTargetsSingleInclude(cond) - resp2 := instance.SetExecution(ctx, t, includeCond, includeTargets) - - response.Result[0] = &action.Execution{ - Condition: includeCond, - CreationDate: resp2.GetSetDate(), - ChangeDate: resp2.GetSetDate(), - Targets: includeTargets, - } - }, - req: &action.ListExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{{ - Filter: &action.ExecutionSearchFilter_IncludeFilter{ - IncludeFilter: &action.IncludeFilter{}, - }, - }}, - }, - }, - want: &action.ListExecutionsResponse{ - Pagination: &filter.PaginationResponse{ - TotalResult: 1, - AppliedLimit: 100, - }, - Result: []*action.Execution{ - {}, - }, - }, }, { name: "list multiple conditions", @@ -659,33 +603,30 @@ func TestServer_ListExecutions(t *testing.T) { } cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0] - targets1 := executionTargetsSingleTarget(targetResp.GetId()) - resp1 := instance.SetExecution(ctx, t, cond1, targets1) + resp1 := instance.SetExecution(ctx, t, cond1, []string{targetResp.GetId()}) response.Result[2] = &action.Execution{ CreationDate: resp1.GetSetDate(), ChangeDate: resp1.GetSetDate(), Condition: cond1, - Targets: targets1, + Targets: []string{targetResp.GetId()}, } cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1] - targets2 := executionTargetsSingleTarget(targetResp.GetId()) - resp2 := instance.SetExecution(ctx, t, cond2, targets2) + resp2 := instance.SetExecution(ctx, t, cond2, []string{targetResp.GetId()}) response.Result[1] = &action.Execution{ CreationDate: resp2.GetSetDate(), ChangeDate: resp2.GetSetDate(), Condition: cond2, - Targets: targets2, + Targets: []string{targetResp.GetId()}, } cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2] - targets3 := executionTargetsSingleTarget(targetResp.GetId()) - resp3 := instance.SetExecution(ctx, t, cond3, targets3) + resp3 := instance.SetExecution(ctx, t, cond3, []string{targetResp.GetId()}) response.Result[0] = &action.Execution{ CreationDate: resp3.GetSetDate(), ChangeDate: resp3.GetSetDate(), Condition: cond3, - Targets: targets3, + Targets: []string{targetResp.GetId()}, } }, req: &action.ListExecutionsRequest{ @@ -709,15 +650,14 @@ func TestServer_ListExecutions(t *testing.T) { args: args{ ctx: isolatedIAMOwnerCTX, 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) + resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) response.Result[(len(conditions)-1)-i] = &action.Execution{ CreationDate: resp.GetSetDate(), ChangeDate: resp.GetSetDate(), Condition: cond, - Targets: targets, + Targets: []string{targetResp.GetId()}, } } }, @@ -761,6 +701,63 @@ func TestServer_ListExecutions(t *testing.T) { }, }, }, + { + name: "list multiple conditions all types, sort id", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + conditions := request.Filters[0].GetInConditionsFilter().GetConditions() + for i, cond := range conditions { + resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) + response.Result[i] = &action.Execution{ + CreationDate: resp.GetSetDate(), + ChangeDate: resp.GetSetDate(), + Condition: cond, + Targets: []string{targetResp.GetId()}, + } + } + }, + req: &action.ListExecutionsRequest{ + SortingColumn: gu.Ptr(action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID), + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{ + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "presamlresponse"}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}, + }, + }, + }, + }}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 10, + AppliedLimit: 100, + }, + Result: []*action.Execution{ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/grpc/action/v2beta/integration_test/server_test.go b/internal/api/grpc/action/v2beta/integration_test/server_test.go index 89a33dd40e..07ee051c63 100644 --- a/internal/api/grpc/action/v2beta/integration_test/server_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/server_test.go @@ -7,14 +7,6 @@ import ( "os" "testing" "time" - - "github.com/muhlemmer/gu" - "github.com/stretchr/testify/assert" - "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" ) var ( @@ -29,41 +21,3 @@ func TestMain(m *testing.M) { return m.Run() }()) } - -func ensureFeatureEnabled(t *testing.T, instance *integration.Instance) { - ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ - Inheritance: true, - }) - require.NoError(t, err) - if f.Actions.GetEnabled() { - return - } - _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ - Actions: gu.Ptr(true), - }) - require.NoError(t, err) - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) - require.EventuallyWithT(t, - func(ttt *assert.CollectT) { - f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ - Inheritance: true, - }) - assert.NoError(ttt, err) - assert.True(ttt, f.Actions.GetEnabled()) - }, - retryDuration, - tick, - "timed out waiting for ensuring instance feature") - - retryDuration, tick = integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) - require.EventuallyWithT(t, - func(ttt *assert.CollectT) { - _, err := instance.Client.ActionV2beta.ListExecutionMethods(ctx, &action.ListExecutionMethodsRequest{}) - assert.NoError(ttt, err) - }, - retryDuration, - tick, - "timed out waiting for ensuring instance feature call") -} diff --git a/internal/api/grpc/action/v2beta/integration_test/target_test.go b/internal/api/grpc/action/v2beta/integration_test/target_test.go index 2fda64b86a..8238d3146d 100644 --- a/internal/api/grpc/action/v2beta/integration_test/target_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/target_test.go @@ -19,7 +19,6 @@ import ( func TestServer_CreateTarget(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) type want struct { id bool @@ -244,7 +243,6 @@ func assertCreateTargetResponse(t *testing.T, creationDate, changeDate time.Time 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 @@ -463,7 +461,6 @@ func assertUpdateTargetResponse(t *testing.T, creationDate, changeDate time.Time func TestServer_DeleteTarget(t *testing.T) { instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) tests := []struct { name string diff --git a/internal/api/grpc/action/v2beta/query.go b/internal/api/grpc/action/v2beta/query.go index d8d6cd3e95..66bafa4e7d 100644 --- a/internal/api/grpc/action/v2beta/query.go +++ b/internal/api/grpc/action/v2beta/query.go @@ -23,10 +23,6 @@ const ( ) func (s *Server) GetTarget(ctx context.Context, req *action.GetTargetRequest) (*action.GetTargetResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } - resp, err := s.query.GetTargetByID(ctx, req.GetId()) if err != nil { return nil, err @@ -46,9 +42,6 @@ type Context interface { } 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.ListTargetsRequestToModel(req) if err != nil { return nil, err @@ -64,9 +57,6 @@ func (s *Server) ListTargets(ctx context.Context, req *action.ListTargetsRequest } 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.ListExecutionsRequestToModel(req) if err != nil { return nil, err @@ -252,12 +242,6 @@ func executionQueryToQuery(searchQuery *action.ExecutionSearchFilter) (query.Sea return inConditionsQueryToQuery(q.InConditionsFilter) case *action.ExecutionSearchFilter_ExecutionTypeFilter: return executionTypeToQuery(q.ExecutionTypeFilter) - case *action.ExecutionSearchFilter_IncludeFilter: - include, err := conditionToInclude(q.IncludeFilter.GetInclude()) - if err != nil { - return nil, err - } - return query.NewIncludeSearchQuery(include) case *action.ExecutionSearchFilter_TargetFilter: return query.NewTargetSearchQuery(q.TargetFilter.GetTargetId()) default: @@ -333,14 +317,12 @@ func executionsToPb(executions []*query.Execution) []*action.Execution { } func executionToPb(e *query.Execution) *action.Execution { - targets := make([]*action.ExecutionTargetType, len(e.Targets)) + targets := make([]string, len(e.Targets)) for i := range e.Targets { switch e.Targets[i].Type { - case domain.ExecutionTargetTypeInclude: - targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Include{Include: executionIDToCondition(e.Targets[i].Target)}} case domain.ExecutionTargetTypeTarget: - targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Target{Target: e.Targets[i].Target}} - case domain.ExecutionTargetTypeUnspecified: + targets[i] = e.Targets[i].Target + case domain.ExecutionTargetTypeInclude, domain.ExecutionTargetTypeUnspecified: continue default: continue diff --git a/internal/api/grpc/action/v2beta/server.go b/internal/api/grpc/action/v2beta/server.go index 069f456ceb..ef0d8eb2ba 100644 --- a/internal/api/grpc/action/v2beta/server.go +++ b/internal/api/grpc/action/v2beta/server.go @@ -1,8 +1,6 @@ package action import ( - "context" - "google.golang.org/grpc" "github.com/zitadel/zitadel/internal/api/authz" @@ -10,7 +8,6 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) @@ -65,10 +62,3 @@ func (s *Server) AuthMethods() authz.MethodMapping { func (s *Server) RegisterGateway() server.RegisterGatewayFunc { return action.RegisterActionServiceHandler } - -func checkActionsEnabled(ctx context.Context) error { - if authz.GetInstance(ctx).Features().Actions { - return nil - } - return zerrors.ThrowPreconditionFailed(nil, "ACTION-8o6pvqfjhs", "Errors.Action.NotEnabled") -} diff --git a/internal/api/grpc/action/v2beta/target.go b/internal/api/grpc/action/v2beta/target.go index 7dc636f29a..26c88b9683 100644 --- a/internal/api/grpc/action/v2beta/target.go +++ b/internal/api/grpc/action/v2beta/target.go @@ -14,9 +14,6 @@ import ( ) func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } add := createTargetToCommand(req) instanceID := authz.GetInstance(ctx).InstanceID() createdAt, err := s.command.AddTarget(ctx, add, instanceID) @@ -35,9 +32,6 @@ func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetReque } 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() update := updateTargetToCommand(req) changedAt, err := s.command.ChangeTarget(ctx, update, instanceID) @@ -55,9 +49,6 @@ func (s *Server) UpdateTarget(ctx context.Context, req *action.UpdateTargetReque } func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } instanceID := authz.GetInstance(ctx).InstanceID() deletedAt, err := s.command.DeleteTarget(ctx, req.GetId(), instanceID) if err != nil { diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go index 60cf569082..baa45c6c6e 100644 --- a/internal/api/grpc/feature/v2/converter.go +++ b/internal/api/grpc/feature/v2/converter.go @@ -22,7 +22,6 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) (*command TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections, LegacyIntrospection: req.OidcLegacyIntrospection, UserSchema: req.UserSchema, - Actions: req.Actions, TokenExchange: req.OidcTokenExchange, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, @@ -41,7 +40,6 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent), @@ -62,7 +60,6 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) (*com LegacyIntrospection: req.OidcLegacyIntrospection, UserSchema: req.UserSchema, TokenExchange: req.OidcTokenExchange, - Actions: req.Actions, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), WebKey: req.WebKey, DebugOIDCParentError: req.DebugOidcParentError, @@ -83,7 +80,6 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), WebKey: featureSourceToFlagPb(&f.WebKey), DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), diff --git a/internal/api/grpc/feature/v2/converter_test.go b/internal/api/grpc/feature/v2/converter_test.go index 62cf701eec..f481e4f65a 100644 --- a/internal/api/grpc/feature/v2/converter_test.go +++ b/internal/api/grpc/feature/v2/converter_test.go @@ -23,7 +23,6 @@ func Test_systemFeaturesToCommand(t *testing.T) { OidcTriggerIntrospectionProjections: gu.Ptr(false), OidcLegacyIntrospection: nil, UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), ImprovedPerformance: nil, OidcSingleV1SessionTermination: gu.Ptr(true), @@ -37,7 +36,6 @@ func Test_systemFeaturesToCommand(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: nil, UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), TokenExchange: gu.Ptr(true), ImprovedPerformance: nil, OIDCSingleV1SessionTermination: gu.Ptr(true), @@ -74,10 +72,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: true, }, - Actions: query.FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, TokenExchange: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -132,10 +126,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Enabled: false, Source: feature_pb.Source_SOURCE_SYSTEM, }, - Actions: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_SYSTEM, - }, ImprovedPerformance: &feature_pb.ImprovedPerformanceFeatureFlag{ ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, Source: feature_pb.Source_SOURCE_SYSTEM, @@ -173,7 +163,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { OidcLegacyIntrospection: nil, UserSchema: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), - Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), DebugOidcParentError: gu.Ptr(true), @@ -191,7 +180,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { LegacyIntrospection: nil, UserSchema: gu.Ptr(true), TokenExchange: gu.Ptr(true), - Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), DebugOIDCParentError: gu.Ptr(true), @@ -231,10 +219,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelInstance, Value: true, }, - Actions: query.FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, TokenExchange: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -293,10 +277,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_INSTANCE, }, - Actions: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_INSTANCE, - }, OidcTokenExchange: &feature_pb.FeatureFlag{ Enabled: false, Source: feature_pb.Source_SOURCE_SYSTEM, diff --git a/internal/api/grpc/feature/v2/integration_test/feature_test.go b/internal/api/grpc/feature/v2/integration_test/feature_test.go index 8d6c295350..f27b57ff8c 100644 --- a/internal/api/grpc/feature/v2/integration_test/feature_test.go +++ b/internal/api/grpc/feature/v2/integration_test/feature_test.go @@ -211,7 +211,6 @@ func TestServer_GetSystemFeatures(t *testing.T) { assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections) assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection) assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema) - assertFeatureFlag(t, tt.want.Actions, got.Actions) }) } } @@ -374,10 +373,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - Actions: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, }, }, { @@ -387,7 +382,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { LoginDefaultOrg: gu.Ptr(true), OidcTriggerIntrospectionProjections: gu.Ptr(false), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), }) require.NoError(t, err) }, @@ -408,10 +402,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: true, Source: feature.Source_SOURCE_INSTANCE, }, - Actions: &feature.FeatureFlag{ - Enabled: true, - Source: feature.Source_SOURCE_INSTANCE, - }, }, }, { @@ -445,10 +435,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - Actions: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, }, }, } diff --git a/internal/api/grpc/feature/v2beta/converter.go b/internal/api/grpc/feature/v2beta/converter.go index 39f2284beb..bbb375716e 100644 --- a/internal/api/grpc/feature/v2beta/converter.go +++ b/internal/api/grpc/feature/v2beta/converter.go @@ -14,7 +14,6 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command. TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections, LegacyIntrospection: req.OidcLegacyIntrospection, UserSchema: req.UserSchema, - Actions: req.Actions, TokenExchange: req.OidcTokenExchange, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, @@ -29,7 +28,6 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), } @@ -42,7 +40,6 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm LegacyIntrospection: req.OidcLegacyIntrospection, UserSchema: req.UserSchema, TokenExchange: req.OidcTokenExchange, - Actions: req.Actions, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), WebKey: req.WebKey, DebugOIDCParentError: req.DebugOidcParentError, @@ -58,7 +55,6 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), WebKey: featureSourceToFlagPb(&f.WebKey), DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), diff --git a/internal/api/grpc/feature/v2beta/converter_test.go b/internal/api/grpc/feature/v2beta/converter_test.go index 2896d8f77b..72d91b10d4 100644 --- a/internal/api/grpc/feature/v2beta/converter_test.go +++ b/internal/api/grpc/feature/v2beta/converter_test.go @@ -22,7 +22,6 @@ func Test_systemFeaturesToCommand(t *testing.T) { OidcTriggerIntrospectionProjections: gu.Ptr(false), OidcLegacyIntrospection: nil, UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), ImprovedPerformance: nil, OidcSingleV1SessionTermination: gu.Ptr(true), @@ -32,7 +31,6 @@ func Test_systemFeaturesToCommand(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: nil, UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), TokenExchange: gu.Ptr(true), ImprovedPerformance: nil, OIDCSingleV1SessionTermination: gu.Ptr(true), @@ -64,10 +62,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: true, }, - Actions: query.FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, TokenExchange: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -107,10 +101,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Enabled: false, Source: feature_pb.Source_SOURCE_SYSTEM, }, - Actions: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_SYSTEM, - }, ImprovedPerformance: &feature_pb.ImprovedPerformanceFeatureFlag{ ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, Source: feature_pb.Source_SOURCE_SYSTEM, @@ -131,7 +121,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { OidcLegacyIntrospection: nil, UserSchema: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), - Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), OidcSingleV1SessionTermination: gu.Ptr(true), @@ -142,7 +131,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { LegacyIntrospection: nil, UserSchema: gu.Ptr(true), TokenExchange: gu.Ptr(true), - Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), OIDCSingleV1SessionTermination: gu.Ptr(true), @@ -174,10 +162,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelInstance, Value: true, }, - Actions: query.FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, TokenExchange: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -217,10 +201,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_INSTANCE, }, - Actions: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_INSTANCE, - }, OidcTokenExchange: &feature_pb.FeatureFlag{ Enabled: false, Source: feature_pb.Source_SOURCE_SYSTEM, diff --git a/internal/api/grpc/feature/v2beta/integration_test/feature_test.go b/internal/api/grpc/feature/v2beta/integration_test/feature_test.go index 69e05352d0..cbd9f5f939 100644 --- a/internal/api/grpc/feature/v2beta/integration_test/feature_test.go +++ b/internal/api/grpc/feature/v2beta/integration_test/feature_test.go @@ -202,10 +202,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - Actions: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, }, }, { @@ -215,7 +211,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { LoginDefaultOrg: gu.Ptr(true), OidcTriggerIntrospectionProjections: gu.Ptr(false), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), }) require.NoError(t, err) }, @@ -236,10 +231,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: true, Source: feature.Source_SOURCE_INSTANCE, }, - Actions: &feature.FeatureFlag{ - Enabled: true, - Source: feature.Source_SOURCE_INSTANCE, - }, }, }, { @@ -273,10 +264,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - Actions: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, }, }, } diff --git a/internal/api/grpc/server/middleware/execution_interceptor.go b/internal/api/grpc/server/middleware/execution_interceptor.go index 053386caae..4aeea6c4da 100644 --- a/internal/api/grpc/server/middleware/execution_interceptor.go +++ b/internal/api/grpc/server/middleware/execution_interceptor.go @@ -5,12 +5,13 @@ import ( "encoding/json" "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/zerrors" ) func ExecutionHandler(queries *query.Queries) grpc.UnaryServerInterceptor { @@ -48,7 +49,7 @@ func executeTargetsForRequest(ctx context.Context, targets []execution.Target, f ProjectID: ctxData.ProjectID, OrgID: ctxData.OrgID, UserID: ctxData.UserID, - Request: req, + Request: Message{req.(proto.Message)}, } return execution.CallTargets(ctx, targets, info) @@ -70,8 +71,8 @@ func executeTargetsForResponse(ctx context.Context, targets []execution.Target, ProjectID: ctxData.ProjectID, OrgID: ctxData.OrgID, UserID: ctxData.UserID, - Request: req, - Response: resp, + Request: Message{req.(proto.Message)}, + Response: Message{resp.(proto.Message)}, } return execution.CallTargets(ctx, targets, info) @@ -80,12 +81,28 @@ func executeTargetsForResponse(ctx context.Context, targets []execution.Target, var _ execution.ContextInfo = &ContextInfoRequest{} type ContextInfoRequest struct { - FullMethod string `json:"fullMethod,omitempty"` - InstanceID string `json:"instanceID,omitempty"` - OrgID string `json:"orgID,omitempty"` - ProjectID string `json:"projectID,omitempty"` - UserID string `json:"userID,omitempty"` - Request interface{} `json:"request,omitempty"` + FullMethod string `json:"fullMethod,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + OrgID string `json:"orgID,omitempty"` + ProjectID string `json:"projectID,omitempty"` + UserID string `json:"userID,omitempty"` + Request Message `json:"request,omitempty"` +} + +type Message struct { + proto.Message +} + +func (r *Message) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r.Message) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *Message) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r.Message) } func (c *ContextInfoRequest) GetHTTPRequestBody() []byte { @@ -97,26 +114,23 @@ func (c *ContextInfoRequest) GetHTTPRequestBody() []byte { } func (c *ContextInfoRequest) SetHTTPResponseBody(resp []byte) error { - if !json.Valid(resp) { - return zerrors.ThrowPreconditionFailed(nil, "ACTION-4m9s2", "Errors.Execution.ResponseIsNotValidJSON") - } - return json.Unmarshal(resp, c.Request) + return json.Unmarshal(resp, &c.Request) } func (c *ContextInfoRequest) GetContent() interface{} { - return c.Request + return c.Request.Message } var _ execution.ContextInfo = &ContextInfoResponse{} type ContextInfoResponse struct { - FullMethod string `json:"fullMethod,omitempty"` - InstanceID string `json:"instanceID,omitempty"` - OrgID string `json:"orgID,omitempty"` - ProjectID string `json:"projectID,omitempty"` - UserID string `json:"userID,omitempty"` - Request interface{} `json:"request,omitempty"` - Response interface{} `json:"response,omitempty"` + FullMethod string `json:"fullMethod,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + OrgID string `json:"orgID,omitempty"` + ProjectID string `json:"projectID,omitempty"` + UserID string `json:"userID,omitempty"` + Request Message `json:"request,omitempty"` + Response Message `json:"response,omitempty"` } func (c *ContextInfoResponse) GetHTTPRequestBody() []byte { @@ -128,9 +142,9 @@ func (c *ContextInfoResponse) GetHTTPRequestBody() []byte { } func (c *ContextInfoResponse) SetHTTPResponseBody(resp []byte) error { - return json.Unmarshal(resp, c.Response) + return json.Unmarshal(resp, &c.Response) } func (c *ContextInfoResponse) GetContent() interface{} { - return c.Response + return c.Response.Message } diff --git a/internal/api/grpc/server/middleware/execution_interceptor_test.go b/internal/api/grpc/server/middleware/execution_interceptor_test.go index 6a5b74c5e4..281db4617a 100644 --- a/internal/api/grpc/server/middleware/execution_interceptor_test.go +++ b/internal/api/grpc/server/middleware/execution_interceptor_test.go @@ -11,6 +11,9 @@ import ( "time" "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/execution" @@ -54,28 +57,28 @@ func (e *mockExecutionTarget) GetSigningKey() string { return e.SigningKey } -type mockContentRequest struct { - Content string -} - -func newMockContentRequest(content string) *mockContentRequest { - return &mockContentRequest{ - Content: content, +func newMockContentRequest(content string) proto.Message { + return &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "content": { + Kind: &structpb.Value_StringValue{StringValue: content}, + }, + }, } } func newMockContextInfoRequest(fullMethod, request string) *ContextInfoRequest { return &ContextInfoRequest{ FullMethod: fullMethod, - Request: newMockContentRequest(request), + Request: Message{Message: newMockContentRequest(request)}, } } func newMockContextInfoResponse(fullMethod, request, response string) *ContextInfoResponse { return &ContextInfoResponse{ FullMethod: fullMethod, - Request: newMockContentRequest(request), - Response: newMockContentRequest(response), + Request: Message{Message: newMockContentRequest(request)}, + Response: Message{Message: newMockContentRequest(response)}, } } @@ -591,7 +594,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { } else { assert.NoError(t, err) } - assert.Equal(t, tt.res.want, resp) + assert.EqualExportedValues(t, tt.res.want, resp) for _, closeF := range closeFuncs { closeF() @@ -632,7 +635,7 @@ func testServerCall( time.Sleep(sleep) w.Header().Set("Content-Type", "application/json") - resp, err := json.Marshal(respBody) + resp, err := protojson.Marshal(respBody.(proto.Message)) if err != nil { http.Error(w, "error", http.StatusInternalServerError) return @@ -723,7 +726,8 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) { statusCode: http.StatusOK, }, }, - req: []byte{}, + req: newMockContentRequest(""), + resp: newMockContentRequest(""), }, res{ wantErr: true, @@ -790,7 +794,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) { } else { assert.NoError(t, err) } - assert.Equal(t, tt.res.want, resp) + assert.EqualExportedValues(t, tt.res.want, resp) for _, closeF := range closeFuncs { closeF() diff --git a/internal/command/instance_features.go b/internal/command/instance_features.go index 3e927cc0c5..cb12bff828 100644 --- a/internal/command/instance_features.go +++ b/internal/command/instance_features.go @@ -21,7 +21,6 @@ type InstanceFeatures struct { LegacyIntrospection *bool UserSchema *bool TokenExchange *bool - Actions *bool ImprovedPerformance []feature.ImprovedPerformanceType WebKey *bool DebugOIDCParentError *bool @@ -39,7 +38,6 @@ func (m *InstanceFeatures) isEmpty() bool { m.LegacyIntrospection == nil && m.UserSchema == nil && m.TokenExchange == nil && - m.Actions == nil && // nil check to allow unset improvements m.ImprovedPerformance == nil && m.WebKey == nil && diff --git a/internal/command/instance_features_model.go b/internal/command/instance_features_model.go index 954c769304..977a46b6c2 100644 --- a/internal/command/instance_features_model.go +++ b/internal/command/instance_features_model.go @@ -71,7 +71,6 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceLegacyIntrospectionEventType, feature_v2.InstanceUserSchemaEventType, feature_v2.InstanceTokenExchangeEventType, - feature_v2.InstanceActionsEventType, feature_v2.InstanceImprovedPerformanceEventType, feature_v2.InstanceWebKeyEventType, feature_v2.InstanceDebugOIDCParentErrorEventType, @@ -108,9 +107,6 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an case feature.KeyUserSchema: v := value.(bool) features.UserSchema = &v - case feature.KeyActions: - v := value.(bool) - features.Actions = &v case feature.KeyImprovedPerformance: v := value.([]feature.ImprovedPerformanceType) features.ImprovedPerformance = v @@ -148,7 +144,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.InstanceLegacyIntrospectionEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.InstanceTokenExchangeEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.InstanceUserSchemaEventType) - cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.InstanceActionsEventType) cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.InstanceImprovedPerformanceEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.WebKey, f.WebKey, feature_v2.InstanceWebKeyEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DebugOIDCParentError, f.DebugOIDCParentError, feature_v2.InstanceDebugOIDCParentErrorEventType) diff --git a/internal/command/instance_features_test.go b/internal/command/instance_features_test.go index e6b6bb4346..02e8896a0c 100644 --- a/internal/command/instance_features_test.go +++ b/internal/command/instance_features_test.go @@ -149,24 +149,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ResourceOwner: "instance1", }, }, - { - name: "set Actions", - eventstore: expectEventstore( - expectFilter(), - expectPush( - feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceActionsEventType, true, - ), - ), - ), - args: args{ctx, &InstanceFeatures{ - Actions: gu.Ptr(true), - }}, - want: &domain.ObjectDetails{ - ResourceOwner: "instance1", - }, - }, { name: "push error", eventstore: expectEventstore( @@ -204,10 +186,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceUserSchemaEventType, true, ), - feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceActionsEventType, true, - ), feature_v2.NewSetEvent[bool]( ctx, aggregate, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, true, @@ -219,7 +197,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: gu.Ptr(true), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), OIDCSingleV1SessionTermination: gu.Ptr(true), }}, want: &domain.ObjectDetails{ diff --git a/internal/command/system_features.go b/internal/command/system_features.go index dc886de318..b317ea93bb 100644 --- a/internal/command/system_features.go +++ b/internal/command/system_features.go @@ -15,7 +15,6 @@ type SystemFeatures struct { LegacyIntrospection *bool TokenExchange *bool UserSchema *bool - Actions *bool ImprovedPerformance []feature.ImprovedPerformanceType OIDCSingleV1SessionTermination *bool DisableUserTokenEvent *bool @@ -30,7 +29,6 @@ func (m *SystemFeatures) isEmpty() bool { m.LegacyIntrospection == nil && m.UserSchema == nil && m.TokenExchange == nil && - m.Actions == nil && // nil check to allow unset improvements m.ImprovedPerformance == nil && m.OIDCSingleV1SessionTermination == nil && diff --git a/internal/command/system_features_model.go b/internal/command/system_features_model.go index 15fc3e0bf0..28e56f8bd4 100644 --- a/internal/command/system_features_model.go +++ b/internal/command/system_features_model.go @@ -64,7 +64,6 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v2.SystemLegacyIntrospectionEventType, feature_v2.SystemUserSchemaEventType, feature_v2.SystemTokenExchangeEventType, - feature_v2.SystemActionsEventType, feature_v2.SystemImprovedPerformanceEventType, feature_v2.SystemOIDCSingleV1SessionTerminationEventType, feature_v2.SystemDisableUserTokenEvent, @@ -98,9 +97,6 @@ func reduceSystemFeature(features *SystemFeatures, key feature.Key, value any) { case feature.KeyTokenExchange: v := value.(bool) features.TokenExchange = &v - case feature.KeyActions: - v := value.(bool) - features.Actions = &v case feature.KeyImprovedPerformance: features.ImprovedPerformance = value.([]feature.ImprovedPerformanceType) case feature.KeyOIDCSingleV1SessionTermination: @@ -128,7 +124,6 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.SystemLegacyIntrospectionEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.SystemUserSchemaEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.SystemTokenExchangeEventType) - cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.SystemActionsEventType) cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.SystemImprovedPerformanceEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.OIDCSingleV1SessionTermination, f.OIDCSingleV1SessionTermination, feature_v2.SystemOIDCSingleV1SessionTerminationEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DisableUserTokenEvent, f.DisableUserTokenEvent, feature_v2.SystemDisableUserTokenEvent) diff --git a/internal/command/system_features_test.go b/internal/command/system_features_test.go index 9c5f4cc2a9..b1b5207b8c 100644 --- a/internal/command/system_features_test.go +++ b/internal/command/system_features_test.go @@ -117,24 +117,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { ResourceOwner: "SYSTEM", }, }, - { - name: "set Actions", - eventstore: expectEventstore( - expectFilter(), - expectPush( - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, true, - ), - ), - ), - args: args{context.Background(), &SystemFeatures{ - Actions: gu.Ptr(true), - }}, - want: &domain.ObjectDetails{ - ResourceOwner: "SYSTEM", - }, - }, { name: "push error", eventstore: expectEventstore( @@ -172,10 +154,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, true, ), - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, true, - ), feature_v2.NewSetEvent[bool]( context.Background(), aggregate, feature_v2.SystemOIDCSingleV1SessionTerminationEventType, true, @@ -187,7 +165,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: gu.Ptr(true), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), OIDCSingleV1SessionTermination: gu.Ptr(true), }}, want: &domain.ObjectDetails{ @@ -233,10 +210,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, true, ), - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, false, - ), feature_v2.NewSetEvent[bool]( context.Background(), aggregate, feature_v2.SystemOIDCSingleV1SessionTerminationEventType, false, @@ -248,7 +221,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: gu.Ptr(true), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(false), OIDCSingleV1SessionTermination: gu.Ptr(false), }}, want: &domain.ObjectDetails{ diff --git a/internal/execution/execution.go b/internal/execution/execution.go index 575c86ecc4..b885858d94 100644 --- a/internal/execution/execution.go +++ b/internal/execution/execution.go @@ -82,11 +82,11 @@ func CallTarget( case domain.TargetTypeCall: return Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), target.GetSigningKey()) case domain.TargetTypeAsync: - go func(target Target, info ContextInfoRequest) { - if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), target.GetSigningKey()); err != nil { + go func(ctx context.Context, target Target, info []byte) { + if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info, target.GetSigningKey()); err != nil { logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err) } - }(target, info) + }(context.WithoutCancel(ctx), target, info.GetHTTPRequestBody()) return nil, nil default: return nil, zerrors.ThrowInternal(nil, "EXEC-auqnansr2m", "Errors.Execution.Unknown") diff --git a/internal/execution/execution_test.go b/internal/execution/execution_test.go index 40731a840a..036b160ab7 100644 --- a/internal/execution/execution_test.go +++ b/internal/execution/execution_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" "github.com/zitadel/zitadel/internal/domain" @@ -149,8 +150,8 @@ func Test_CallTarget(t *testing.T) { info: requestContextInfo1, server: &callTestServer{ method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), timeout: time.Second, statusCode: http.StatusInternalServerError, }, @@ -170,8 +171,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusInternalServerError, }, target: &mockTarget{ @@ -191,8 +192,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusOK, }, target: &mockTarget{ @@ -212,8 +213,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusOK, signingKey: "signingkey", }, @@ -235,8 +236,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusInternalServerError, }, target: &mockTarget{ @@ -256,8 +257,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusOK, }, target: &mockTarget{ @@ -266,7 +267,7 @@ func Test_CallTarget(t *testing.T) { }, }, res{ - body: []byte("{\"request\":\"content2\"}"), + body: []byte("{\"content\":\"request2\"}"), }, }, { @@ -277,8 +278,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusOK, signingKey: "signingkey", }, @@ -289,7 +290,7 @@ func Test_CallTarget(t *testing.T) { }, }, res{ - body: []byte("{\"request\":\"content2\"}"), + body: []byte("{\"content\":\"request2\"}"), }, }, } @@ -576,13 +577,13 @@ func testCallTargets(ctx context.Context, } var requestContextInfo1 = &middleware.ContextInfoRequest{ - Request: &request{ - Request: "content1", - }, + Request: middleware.Message{Message: &structpb.Struct{ + Fields: map[string]*structpb.Value{"content": structpb.NewStringValue("request1")}, + }}, } -var requestContextInfoBody1 = []byte("{\"request\":{\"request\":\"content1\"}}") -var requestContextInfoBody2 = []byte("{\"request\":{\"request\":\"content2\"}}") +var requestContextInfoBody1 = []byte("{\"request\":{\"content\":\"request1\"}}") +var requestContextInfoBody2 = []byte("{\"request\":{\"content\":\"request2\"}}") type request struct { Request string `json:"request"` diff --git a/internal/feature/feature.go b/internal/feature/feature.go index 638917fd68..389b750483 100644 --- a/internal/feature/feature.go +++ b/internal/feature/feature.go @@ -15,7 +15,7 @@ const ( KeyLegacyIntrospection KeyUserSchema KeyTokenExchange - KeyActions + KeyActionsDeprecated KeyImprovedPerformance KeyWebKey KeyDebugOIDCParentError @@ -46,7 +46,6 @@ type Features struct { LegacyIntrospection bool `json:"legacy_introspection,omitempty"` UserSchema bool `json:"user_schema,omitempty"` TokenExchange bool `json:"token_exchange,omitempty"` - Actions bool `json:"actions,omitempty"` ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"` WebKey bool `json:"web_key,omitempty"` DebugOIDCParentError bool `json:"debug_oidc_parent_error,omitempty"` diff --git a/internal/feature/key_enumer.go b/internal/feature/key_enumer.go index 5a37b96270..6466061718 100644 --- a/internal/feature/key_enumer.go +++ b/internal/feature/key_enumer.go @@ -30,7 +30,7 @@ func _KeyNoOp() { _ = x[KeyLegacyIntrospection-(3)] _ = x[KeyUserSchema-(4)] _ = x[KeyTokenExchange-(5)] - _ = x[KeyActions-(6)] + _ = x[KeyActionsDeprecated-(6)] _ = x[KeyImprovedPerformance-(7)] _ = x[KeyWebKey-(8)] _ = x[KeyDebugOIDCParentError-(9)] @@ -42,7 +42,7 @@ func _KeyNoOp() { _ = x[KeyConsoleUseV2UserApi-(15)] } -var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2, KeyConsoleUseV2UserApi} +var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActionsDeprecated, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2, KeyConsoleUseV2UserApi} var _KeyNameToValueMap = map[string]Key{ _KeyName[0:11]: KeyUnspecified, @@ -57,8 +57,8 @@ var _KeyNameToValueMap = map[string]Key{ _KeyLowerName[81:92]: KeyUserSchema, _KeyName[92:106]: KeyTokenExchange, _KeyLowerName[92:106]: KeyTokenExchange, - _KeyName[106:113]: KeyActions, - _KeyLowerName[106:113]: KeyActions, + _KeyName[106:113]: KeyActionsDeprecated, + _KeyLowerName[106:113]: KeyActionsDeprecated, _KeyName[113:133]: KeyImprovedPerformance, _KeyLowerName[113:133]: KeyImprovedPerformance, _KeyName[133:140]: KeyWebKey, diff --git a/internal/integration/action.go b/internal/integration/action.go index b8f69c5788..e849b5c21c 100644 --- a/internal/integration/action.go +++ b/internal/integration/action.go @@ -8,6 +8,9 @@ import ( "reflect" "sync" "time" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" ) type server struct { @@ -100,3 +103,61 @@ func TestServerCall( server.server = httptest.NewServer(http.HandlerFunc(handler)) return server.URL(), server.Close, server.Called, server.ResetCalled } + +func TestServerCallProto( + reqBody interface{}, + sleep time.Duration, + statusCode int, + respBody proto.Message, +) (url string, closeF func(), calledF func() int, resetCalledF func()) { + server := &server{ + called: 0, + } + + handler := func(w http.ResponseWriter, r *http.Request) { + server.Increase() + if reqBody != nil { + data, err := json.Marshal(reqBody) + if err != nil { + http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError) + return + } + sentBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError) + return + } + if !reflect.DeepEqual(data, sentBody) { + http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError) + return + } + } + if statusCode != http.StatusOK { + http.Error(w, "error, statusCode", statusCode) + return + } + + time.Sleep(sleep) + + if respBody != nil { + w.Header().Set("Content-Type", "application/json") + resp, err := protojson.Marshal(respBody) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + if _, err := io.Writer.Write(w, resp); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } else { + if _, err := io.WriteString(w, "finished successfully"); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + } + + server.server = httptest.NewServer(http.HandlerFunc(handler)) + return server.URL(), server.Close, server.Called, server.ResetCalled +} diff --git a/internal/integration/client.go b/internal/integration/client.go index 2cb8fa3641..e82a6bec55 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -763,7 +763,7 @@ func (i *Instance) DeleteExecution(ctx context.Context, t *testing.T, cond *acti require.NoError(t, err) } -func (i *Instance) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse { +func (i *Instance) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []string) *action.SetExecutionResponse { target, err := i.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, Targets: targets, diff --git a/internal/query/instance_features.go b/internal/query/instance_features.go index 911edaa606..4ec40dc9d5 100644 --- a/internal/query/instance_features.go +++ b/internal/query/instance_features.go @@ -14,7 +14,6 @@ type InstanceFeatures struct { LegacyIntrospection FeatureSource[bool] UserSchema FeatureSource[bool] TokenExchange FeatureSource[bool] - Actions FeatureSource[bool] ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] WebKey FeatureSource[bool] DebugOIDCParentError FeatureSource[bool] diff --git a/internal/query/instance_features_model.go b/internal/query/instance_features_model.go index 11deb30f34..6a0abbb58c 100644 --- a/internal/query/instance_features_model.go +++ b/internal/query/instance_features_model.go @@ -67,7 +67,6 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceLegacyIntrospectionEventType, feature_v2.InstanceUserSchemaEventType, feature_v2.InstanceTokenExchangeEventType, - feature_v2.InstanceActionsEventType, feature_v2.InstanceImprovedPerformanceEventType, feature_v2.InstanceWebKeyEventType, feature_v2.InstanceDebugOIDCParentErrorEventType, @@ -98,7 +97,6 @@ func (m *InstanceFeaturesReadModel) populateFromSystem() bool { m.instance.LegacyIntrospection = m.system.LegacyIntrospection m.instance.UserSchema = m.system.UserSchema m.instance.TokenExchange = m.system.TokenExchange - m.instance.Actions = m.system.Actions m.instance.ImprovedPerformance = m.system.ImprovedPerformance m.instance.OIDCSingleV1SessionTermination = m.system.OIDCSingleV1SessionTermination m.instance.DisableUserTokenEvent = m.system.DisableUserTokenEvent @@ -113,7 +111,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_ return err } switch key { - case feature.KeyUnspecified: + case feature.KeyUnspecified, + feature.KeyActionsDeprecated: return nil case feature.KeyLoginDefaultOrg: features.LoginDefaultOrg.set(level, event.Value) @@ -125,8 +124,6 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_ features.UserSchema.set(level, event.Value) case feature.KeyTokenExchange: features.TokenExchange.set(level, event.Value) - case feature.KeyActions: - features.Actions.set(level, event.Value) case feature.KeyImprovedPerformance: features.ImprovedPerformance.set(level, event.Value) case feature.KeyWebKey: diff --git a/internal/query/instance_features_test.go b/internal/query/instance_features_test.go index 903c2872a9..d80a3b05fc 100644 --- a/internal/query/instance_features_test.go +++ b/internal/query/instance_features_test.go @@ -105,10 +105,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent( - ctx, aggregate, - feature_v2.InstanceActionsEventType, false, - )), ), ), args: args{true}, @@ -132,10 +128,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelInstance, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: false, - }, }, }, { @@ -162,10 +154,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent( - ctx, aggregate, - feature_v2.InstanceActionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( ctx, aggregate, feature_v2.InstanceResetEventType, @@ -197,10 +185,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, { @@ -223,10 +207,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent( - ctx, aggregate, - feature_v2.InstanceActionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( ctx, aggregate, feature_v2.InstanceResetEventType, @@ -258,10 +238,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, } diff --git a/internal/query/projection/instance_features.go b/internal/query/projection/instance_features.go index 798af6693c..34100a0d66 100644 --- a/internal/query/projection/instance_features.go +++ b/internal/query/projection/instance_features.go @@ -80,10 +80,6 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.InstanceTokenExchangeEventType, Reduce: reduceInstanceSetFeature[bool], }, - { - Event: feature_v2.InstanceActionsEventType, - Reduce: reduceInstanceSetFeature[bool], - }, { Event: feature_v2.InstanceImprovedPerformanceEventType, Reduce: reduceInstanceSetFeature[[]feature.ImprovedPerformanceType], diff --git a/internal/query/projection/system_features.go b/internal/query/projection/system_features.go index f6f0a36d56..de54054e78 100644 --- a/internal/query/projection/system_features.go +++ b/internal/query/projection/system_features.go @@ -72,10 +72,6 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.SystemTokenExchangeEventType, Reduce: reduceSystemSetFeature[bool], }, - { - Event: feature_v2.SystemActionsEventType, - Reduce: reduceSystemSetFeature[bool], - }, { Event: feature_v2.SystemImprovedPerformanceEventType, Reduce: reduceSystemSetFeature[[]feature.ImprovedPerformanceType], diff --git a/internal/query/system_features.go b/internal/query/system_features.go index 31ad402d12..dcbbb7d6fe 100644 --- a/internal/query/system_features.go +++ b/internal/query/system_features.go @@ -25,7 +25,6 @@ type SystemFeatures struct { LegacyIntrospection FeatureSource[bool] UserSchema FeatureSource[bool] TokenExchange FeatureSource[bool] - Actions FeatureSource[bool] ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] OIDCSingleV1SessionTermination FeatureSource[bool] DisableUserTokenEvent FeatureSource[bool] diff --git a/internal/query/system_features_model.go b/internal/query/system_features_model.go index 217154e3ed..69e1f35968 100644 --- a/internal/query/system_features_model.go +++ b/internal/query/system_features_model.go @@ -60,7 +60,6 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v2.SystemLegacyIntrospectionEventType, feature_v2.SystemUserSchemaEventType, feature_v2.SystemTokenExchangeEventType, - feature_v2.SystemActionsEventType, feature_v2.SystemImprovedPerformanceEventType, feature_v2.SystemOIDCSingleV1SessionTerminationEventType, feature_v2.SystemDisableUserTokenEvent, @@ -82,7 +81,8 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S return err } switch key { - case feature.KeyUnspecified: + case feature.KeyUnspecified, + feature.KeyActionsDeprecated: return nil case feature.KeyLoginDefaultOrg: features.LoginDefaultOrg.set(level, event.Value) @@ -94,8 +94,6 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S features.UserSchema.set(level, event.Value) case feature.KeyTokenExchange: features.TokenExchange.set(level, event.Value) - case feature.KeyActions: - features.Actions.set(level, event.Value) case feature.KeyImprovedPerformance: features.ImprovedPerformance.set(level, event.Value) case feature.KeyOIDCSingleV1SessionTermination: diff --git a/internal/query/system_features_test.go b/internal/query/system_features_test.go index fcd0f812f5..5a58ac23d7 100644 --- a/internal/query/system_features_test.go +++ b/internal/query/system_features_test.go @@ -61,10 +61,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, true, - )), ), ), want: &SystemFeatures{ @@ -87,10 +83,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { Level: feature.LevelSystem, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, }, }, { @@ -113,10 +105,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( context.Background(), aggregate, feature_v2.SystemResetEventType, @@ -147,10 +135,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, { @@ -173,10 +157,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( context.Background(), aggregate, feature_v2.SystemResetEventType, @@ -207,10 +187,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, } diff --git a/internal/repository/execution/queue.go b/internal/repository/execution/queue.go index 28f8edbf31..ed3a6ce4a0 100644 --- a/internal/repository/execution/queue.go +++ b/internal/repository/execution/queue.go @@ -41,16 +41,16 @@ func ContextInfoFromRequest(e *Request) *ContextInfoEvent { } 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"` + AggregateID string `json:"aggregateID,omitempty"` + AggregateType string `json:"aggregateType,omitempty"` + ResourceOwner string `json:"resourceOwner,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + Version string `json:"version,omitempty"` + Sequence uint64 `json:"sequence,omitempty"` + EventType string `json:"event_type,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UserID string `json:"userID,omitempty"` + EventPayload json.RawMessage `json:"event_payload,omitempty"` } func (c *ContextInfoEvent) GetHTTPRequestBody() []byte { diff --git a/internal/repository/feature/feature_v2/eventstore.go b/internal/repository/feature/feature_v2/eventstore.go index e8d0da1ab0..00618f56c2 100644 --- a/internal/repository/feature/feature_v2/eventstore.go +++ b/internal/repository/feature/feature_v2/eventstore.go @@ -12,7 +12,6 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, SystemLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]]) - eventstore.RegisterFilterEventMapper(AggregateType, SystemActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemOIDCSingleV1SessionTerminationEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemDisableUserTokenEvent, eventstore.GenericEventMapper[SetEvent[bool]]) @@ -26,7 +25,6 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, InstanceLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]]) - eventstore.RegisterFilterEventMapper(AggregateType, InstanceActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceWebKeyEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceDebugOIDCParentErrorEventType, eventstore.GenericEventMapper[SetEvent[bool]]) diff --git a/internal/repository/feature/feature_v2/feature.go b/internal/repository/feature/feature_v2/feature.go index 008986824b..d5e8941df2 100644 --- a/internal/repository/feature/feature_v2/feature.go +++ b/internal/repository/feature/feature_v2/feature.go @@ -17,7 +17,6 @@ var ( SystemLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLegacyIntrospection) SystemUserSchemaEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyUserSchema) SystemTokenExchangeEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTokenExchange) - SystemActionsEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyActions) SystemImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyImprovedPerformance) SystemOIDCSingleV1SessionTerminationEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyOIDCSingleV1SessionTermination) SystemDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelSystem, feature.KeyDisableUserTokenEvent) @@ -31,7 +30,6 @@ var ( InstanceLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLegacyIntrospection) InstanceUserSchemaEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyUserSchema) InstanceTokenExchangeEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTokenExchange) - InstanceActionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyActions) InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance) InstanceWebKeyEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyWebKey) InstanceDebugOIDCParentErrorEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDebugOIDCParentError) diff --git a/proto/buf.yaml b/proto/buf.yaml index f8cf192a95..31bc7b4ccc 100644 --- a/proto/buf.yaml +++ b/proto/buf.yaml @@ -7,6 +7,10 @@ deps: breaking: use: - FILE + - FIELD_NO_DELETE_UNLESS_NAME_RESERVED + - FIELD_NO_DELETE_UNLESS_NUMBER_RESERVED + except: + - FIELD_NO_DELETE ignore_unstable_packages: true lint: use: diff --git a/proto/zitadel/action/v2beta/action_service.proto b/proto/zitadel/action/v2beta/action_service.proto index d1eebfa344..f225905225 100644 --- a/proto/zitadel/action/v2beta/action_service.proto +++ b/proto/zitadel/action/v2beta/action_service.proto @@ -670,8 +670,8 @@ message ListTargetsResponse { 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; + // Ordered list of targets called during the execution. + repeated string targets = 2; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { example: "{\"condition\":{\"request\":{\"method\":\"zitadel.session.v2.SessionService/ListSessions\"}},\"targets\":[{\"target\":\"69629026806489455\"}]}"; }; diff --git a/proto/zitadel/action/v2beta/execution.proto b/proto/zitadel/action/v2beta/execution.proto index 4f4ad1ce88..e93470e5dc 100644 --- a/proto/zitadel/action/v2beta/execution.proto +++ b/proto/zitadel/action/v2beta/execution.proto @@ -29,18 +29,8 @@ message Execution { example: "\"2025-01-23T10:34:18.051Z\""; } ]; - // Ordered list of targets/includes called during the execution. - repeated ExecutionTargetType targets = 4; -} - -message ExecutionTargetType { - oneof type { - option (validate.required) = true; - // Unique identifier of existing target to call. - string target = 1; - // Unique identifier of existing execution to include targets of. - Condition include = 2; - } + // Ordered list of targets called during the execution. + repeated string targets = 4; } message Condition { diff --git a/proto/zitadel/action/v2beta/query.proto b/proto/zitadel/action/v2beta/query.proto index 564db8bc9f..fe4f72f294 100644 --- a/proto/zitadel/action/v2beta/query.proto +++ b/proto/zitadel/action/v2beta/query.proto @@ -19,7 +19,6 @@ message ExecutionSearchFilter { InConditionsFilter in_conditions_filter = 1; ExecutionTypeFilter execution_type_filter = 2; TargetFilter target_filter = 3; - IncludeFilter include_filter = 4; } } @@ -43,16 +42,6 @@ message TargetFilter { ]; } -message IncludeFilter { - // Defines the include to query for. - Condition include = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the id of the include" - example: "\"request.zitadel.session.v2.SessionService\""; - } - ]; -} - enum TargetFieldName { TARGET_FIELD_NAME_UNSPECIFIED = 0; TARGET_FIELD_NAME_ID = 1; diff --git a/proto/zitadel/feature/v2/instance.proto b/proto/zitadel/feature/v2/instance.proto index efd7f83e4c..fe8d3f7a39 100644 --- a/proto/zitadel/feature/v2/instance.proto +++ b/proto/zitadel/feature/v2/instance.proto @@ -11,6 +11,8 @@ import "zitadel/feature/v2/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; message SetInstanceFeaturesRequest{ + reserved 6; + reserved "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -43,12 +45,6 @@ message SetInstanceFeaturesRequest{ description: "Enable the experimental `urn:ietf:params:oauth:grant-type:token-exchange` grant type for the OIDC token endpoint. Token exchange can be used to request tokens with a lesser scope or impersonate other users. See the security policy to allow impersonation on an instance."; } ]; - optional bool actions = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; repeated ImprovedPerformance improved_performance = 7 [ (validate.rules).repeated.unique = true, @@ -135,6 +131,8 @@ message GetInstanceFeaturesRequest { } message GetInstanceFeaturesResponse { + reserved 7; + reserved "actions"; zitadel.object.v2.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -171,13 +169,6 @@ message GetInstanceFeaturesResponse { } ]; - FeatureFlag actions = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - ImprovedPerformanceFeatureFlag improved_performance = 8 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[1]"; diff --git a/proto/zitadel/feature/v2/system.proto b/proto/zitadel/feature/v2/system.proto index c734905fb2..d222e2a90c 100644 --- a/proto/zitadel/feature/v2/system.proto +++ b/proto/zitadel/feature/v2/system.proto @@ -11,6 +11,8 @@ import "zitadel/feature/v2/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; message SetSystemFeaturesRequest{ + reserved 6; + reserved "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -46,13 +48,6 @@ message SetSystemFeaturesRequest{ } ]; - optional bool actions = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - repeated ImprovedPerformance improved_performance = 7 [ (validate.rules).repeated.unique = true, (validate.rules).repeated.items.enum = {defined_only: true, not_in: [0]}, @@ -110,6 +105,8 @@ message ResetSystemFeaturesResponse { message GetSystemFeaturesRequest {} message GetSystemFeaturesResponse { + reserved 7; + reserved "actions"; zitadel.object.v2.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -146,13 +143,6 @@ message GetSystemFeaturesResponse { } ]; - FeatureFlag actions = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - ImprovedPerformanceFeatureFlag improved_performance = 8 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[1]"; diff --git a/proto/zitadel/feature/v2beta/instance.proto b/proto/zitadel/feature/v2beta/instance.proto index 865a1d2308..7717dd7556 100644 --- a/proto/zitadel/feature/v2beta/instance.proto +++ b/proto/zitadel/feature/v2beta/instance.proto @@ -11,6 +11,8 @@ import "zitadel/feature/v2beta/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature"; message SetInstanceFeaturesRequest{ + reserved 6; + reserved "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -43,12 +45,6 @@ message SetInstanceFeaturesRequest{ description: "Enable the experimental `urn:ietf:params:oauth:grant-type:token-exchange` grant type for the OIDC token endpoint. Token exchange can be used to request tokens with a lesser scope or impersonate other users. See the security policy to allow impersonation on an instance."; } ]; - optional bool actions = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; repeated ImprovedPerformance improved_performance = 7 [ (validate.rules).repeated.unique = true, @@ -101,6 +97,8 @@ message GetInstanceFeaturesRequest { } message GetInstanceFeaturesResponse { + reserved 7; + reserved "actions"; zitadel.object.v2beta.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -137,13 +135,6 @@ message GetInstanceFeaturesResponse { } ]; - FeatureFlag actions = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - ImprovedPerformanceFeatureFlag improved_performance = 8 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[1]"; diff --git a/proto/zitadel/feature/v2beta/system.proto b/proto/zitadel/feature/v2beta/system.proto index 98b37ad893..624e68ec79 100644 --- a/proto/zitadel/feature/v2beta/system.proto +++ b/proto/zitadel/feature/v2beta/system.proto @@ -11,6 +11,8 @@ import "zitadel/feature/v2beta/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature"; message SetSystemFeaturesRequest{ + reserved 6; + reserved "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -46,13 +48,6 @@ message SetSystemFeaturesRequest{ } ]; - optional bool actions = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - repeated ImprovedPerformance improved_performance = 7 [ (validate.rules).repeated.unique = true, (validate.rules).repeated.items.enum = {defined_only: true, not_in: [0]}, @@ -83,6 +78,8 @@ message ResetSystemFeaturesResponse { message GetSystemFeaturesRequest {} message GetSystemFeaturesResponse { + reserved 7; + reserved "actions"; zitadel.object.v2beta.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -119,13 +116,6 @@ message GetSystemFeaturesResponse { } ]; - FeatureFlag actions = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - ImprovedPerformanceFeatureFlag improved_performance = 8 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[1]"; From a9dd78a13269eb57435a995f17ef05cc07d8ddc2 Mon Sep 17 00:00:00 2001 From: Allen Oyieke Date: Mon, 28 Apr 2025 12:53:31 +0300 Subject: [PATCH 07/26] docs: fix typo in Java SDK example document (#9804) # Which Problems Are Solved This PR resolves the issue #9648 # How the Problems Are Solved Resolves a typo in the documentation # Additional Context - Closes #9648 - Discussion #9648 --- docs/docs/sdk-examples/java.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/sdk-examples/java.mdx b/docs/docs/sdk-examples/java.mdx index e7afb5188b..e4862422d0 100644 --- a/docs/docs/sdk-examples/java.mdx +++ b/docs/docs/sdk-examples/java.mdx @@ -41,7 +41,7 @@ The following features are covered by Java Spring Security: The goal is to have a ZITADEL Java SDK in the future which will cover the following: - Wrapper around Java Spring Security - Authentication with OIDC -- Authorization and checking Rolls +- Authorization and checking Roles - Integrate ZITADEL APIs to read and manage resources - Integrate ZITADEL Session API to create your own login UI From 205beb607b35a08cac07ee6abdb9615db484e614 Mon Sep 17 00:00:00 2001 From: intelli-joe <148788305+intelli-joe@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:22:04 -0500 Subject: [PATCH 08/26] fix: update link to postgres-insecure example in docs (#9802) Fix reference to postgres-insecure example in docs --- docs/docs/self-hosting/manage/configure/_helm.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/self-hosting/manage/configure/_helm.mdx b/docs/docs/self-hosting/manage/configure/_helm.mdx index b35957abb8..17fb8165a6 100644 --- a/docs/docs/self-hosting/manage/configure/_helm.mdx +++ b/docs/docs/self-hosting/manage/configure/_helm.mdx @@ -3,4 +3,4 @@ Configure Zitadel using native Helm values. You can manage secrets through Helm values, letting Helm create Kubernetes secrets. Alternatively, reference existing Kubernetes secrets managed outside of Helm. See the [referenced secrets example](https://github.com/zitadel/zitadel-charts/tree/main/examples/3-referenced-secrets) in the charts */examples* folder. -For a quick setup, check out the [insecure Postgres example](https://github.com/zitadel/zitadel-charts/tree/main/examples/1-insecure-postgres). +For a quick setup, check out the [insecure Postgres example](https://github.com/zitadel/zitadel-charts/tree/main/examples/1-postgres-insecure). From ed4e226da923f4caf49067db8ab2934d849f9ad9 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Tue, 29 Apr 2025 11:12:43 +0200 Subject: [PATCH 09/26] fix(defaults): comment default SystemAPIUsers (#9813) # Which Problems Are Solved If I start a fresh instance and do not overwrite `SystemAPIUsers` I get an error during startup `error="decoding failed due to the following error(s):\n\n'SystemAPIUsers[0][path]' expected a map, got 'string'\n'SystemAPIUsers[0][memberships]' expected a map, got 'slice'"` # How the Problems Are Solved the configuration is commented so that the example is still there # Additional Changes - # Additional Context was added in https://github.com/zitadel/zitadel/pull/9757 --- cmd/defaults.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 8482ccec9f..55e14bbada 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -607,14 +607,14 @@ EncryptionKeys: UserAgentCookieKeyID: "userAgentCookieKey" # ZITADEL_ENCRYPTIONKEYS_USERAGENTCOOKIEKEYID SystemAPIUsers: - - superuser: - Path: /path/to/superuser/key.pem - Memberships: - - MemberType: Organization - Roles: "ORG_OWNER" - AggregateID: "123456789012345678" - - MemberType: Project - Roles: "PROJECT_OWNER" + # - superuser: + # Path: /path/to/superuser/key.pem + # Memberships: + # - MemberType: Organization + # Roles: "ORG_OWNER" + # AggregateID: "123456789012345678" + # - MemberType: Project + # Roles: "PROJECT_OWNER" # # Add keys for authentication of the systemAPI here: From ce823c9176bcd64ff3aacf73ce5fcb192dca76ce Mon Sep 17 00:00:00 2001 From: David Skewis Date: Tue, 29 Apr 2025 10:42:49 +0100 Subject: [PATCH 10/26] fix: update session recordings for posthog (#9775) # Which Problems Are Solved - Updates to only capture 10% of events with posthog # How the Problems Are Solved - Uses a feature flag rolled out to 10% of users to enable the capture # Additional Changes N/A # Additional Context N/A --- console/src/app/services/posthog.service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/console/src/app/services/posthog.service.ts b/console/src/app/services/posthog.service.ts index 2f9630282a..17855d7eb5 100644 --- a/console/src/app/services/posthog.service.ts +++ b/console/src/app/services/posthog.service.ts @@ -26,8 +26,16 @@ export class PosthogService implements OnDestroy { maskAllInputs: true, maskTextSelector: '*', }, + disable_session_recording: true, enable_heatmaps: true, persistence: 'memory', + loaded: (posthog) => { + posthog.onFeatureFlags((flags) => { + if (posthog.isFeatureEnabled('session_recording')) { + posthog.startSessionRecording(); + } + }); + }, }); } } From d930a09cb04012cac96610f281401fe6d094d030 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 29 Apr 2025 13:25:49 +0200 Subject: [PATCH 11/26] fix: Improve Actions V2 Texts and reenable in settings (#9814) # Which Problems Are Solved This pr includes improved texts to make the usage of Actions V2 more easy. Since the removal of the Actions V2 Feature Flag we removed the code that checks if it's enabled in the settings sidenav. # How the Problems Are Solved Added new texts to translations. Removed sidenav logic that checks for Actions V2 Feature Flag # Additional Context - Part of #7248 - Part of #9688 --------- Co-authored-by: Max Peintner Co-authored-by: Max Peintner --- console/package.json | 4 +- .../actions-two-actions-table.component.ts | 4 +- .../actions-two-actions.component.html | 3 + .../actions-two-actions.component.ts | 3 + ...actions-two-add-action-dialog.component.ts | 19 +--- ...tions-two-add-action-target.component.html | 14 +-- ...actions-two-add-action-target.component.ts | 92 ++++++++++--------- ...tions-two-add-target-dialog.component.html | 3 + ...tions-two-add-target-dialog.component.scss | 4 + .../actions-two-targets.component.html | 3 + .../actions-two-targets.component.ts | 6 +- .../modules/actions-two/actions-two.module.ts | 2 + .../src/app/modules/settings-list/settings.ts | 2 + .../modules/sidenav/sidenav.component.html | 1 + .../modules/sidenav/sidenav.component.scss | 4 + .../app/modules/sidenav/sidenav.component.ts | 1 + .../app/pages/actions/actions.component.html | 3 + .../app/pages/actions/actions.component.ts | 63 ++++++------- .../app/pages/instance/instance.component.ts | 30 +----- console/src/assets/i18n/bg.json | 8 +- console/src/assets/i18n/cs.json | 8 +- console/src/assets/i18n/de.json | 8 +- console/src/assets/i18n/en.json | 8 +- console/src/assets/i18n/es.json | 8 +- console/src/assets/i18n/fr.json | 8 +- console/src/assets/i18n/hu.json | 8 +- console/src/assets/i18n/id.json | 8 +- console/src/assets/i18n/it.json | 8 +- console/src/assets/i18n/ja.json | 8 +- console/src/assets/i18n/ko.json | 8 +- console/src/assets/i18n/mk.json | 8 +- console/src/assets/i18n/nl.json | 8 +- console/src/assets/i18n/pl.json | 8 +- console/src/assets/i18n/pt.json | 8 +- console/src/assets/i18n/ro.json | 8 +- console/src/assets/i18n/ru.json | 8 +- console/src/assets/i18n/sv.json | 8 +- console/src/assets/i18n/zh.json | 8 +- console/yarn.lock | 25 ++--- 39 files changed, 249 insertions(+), 189 deletions(-) diff --git a/console/package.json b/console/package.json index 2d986730c2..77a8a40147 100644 --- a/console/package.json +++ b/console/package.json @@ -31,8 +31,8 @@ "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@ngx-translate/core": "^15.0.0", - "@zitadel/client": "^1.0.7", - "@zitadel/proto": "1.0.5-sha-4118a9d", + "@zitadel/client": "1.2.0", + "@zitadel/proto": "1.2.0", "angular-oauth2-oidc": "^15.0.1", "angularx-qrcode": "^16.0.2", "buffer": "^6.0.3", diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts index 2d9942c406..658c205c4e 100644 --- a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts @@ -56,9 +56,9 @@ export class ActionsTwoActionsTableComponent { return executions.map((execution) => { const mappedTargets = execution.targets.map((target) => { - const targetType = targetsMap.get(target.type.value); + const targetType = targetsMap.get(target); if (!targetType) { - throw new Error(`Target with id ${target.type.value} not found`); + throw new Error(`Target with id ${target} not found`); } return targetType; }); diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html index c22b03ef76..3e6c31fc0e 100644 --- a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html @@ -1,4 +1,7 @@

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

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

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

}; -type CorrectlyTypedTargets = { type: Extract }; - -export type CorrectlyTypedExecution = Omit & { +export type CorrectlyTypedExecution = Omit & { condition: CorrectlyTypedCondition; - targets: CorrectlyTypedTargets[]; }; export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExecution => { @@ -48,9 +40,6 @@ export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExec return { ...execution, condition, - targets: execution.targets - .map(({ type }) => ({ type })) - .filter((target): target is CorrectlyTypedTargets => target.type.case === 'target'), }; }; @@ -81,7 +70,7 @@ export class ActionTwoAddActionDialogComponent { protected readonly typeSignal = signal('request'); protected readonly conditionSignal = signal['condition']>(undefined); - protected readonly targetsSignal = signal[]>([]); + protected readonly targetsSignal = signal([]); protected readonly continueSubject = new Subject(); @@ -112,7 +101,7 @@ export class ActionTwoAddActionDialogComponent { this.targetsSignal.set(data.execution.targets); this.typeSignal.set(data.execution.condition.conditionType.case); this.conditionSignal.set(data.execution.condition); - this.preselectedTargetIds = data.execution.targets.map((target) => target.type.value); + this.preselectedTargetIds = data.execution.targets; this.page.set(Page.Target); // Set the initial page based on the provided execution data } diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.html b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.html index 422ed7991e..26d9e3be2c 100644 --- a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.html +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.html @@ -1,4 +1,4 @@ -
+

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

@@ -8,9 +8,9 @@ #trigger="matAutocompleteTrigger" #input type="text" - [formControl]="form().controls.autocomplete" + [formControl]="form.controls.autocomplete" [matAutocomplete]="autoservice" - (keydown.enter)="handleEnter($event); input.blur(); trigger.closePanel()" + (keydown.enter)="handleEnter($event, form); input.blur(); trigger.closePanel()" /> @@ -19,7 +19,7 @@ {{ target.name }} @@ -27,7 +27,7 @@ - +
diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts index 658c205c4e..af9673dbf5 100644 --- a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts @@ -55,13 +55,9 @@ export class ActionsTwoActionsTableComponent { } return executions.map((execution) => { - const mappedTargets = execution.targets.map((target) => { - const targetType = targetsMap.get(target); - if (!targetType) { - throw new Error(`Target with id ${target} not found`); - } - return targetType; - }); + const mappedTargets = execution.targets + .map((target) => targetsMap.get(target)) + .filter((target): target is NonNullable => !!target); return { execution, mappedTargets }; }); }); diff --git a/console/src/app/modules/settings-list/settings.ts b/console/src/app/modules/settings-list/settings.ts index c96431fa30..7ec7fdea15 100644 --- a/console/src/app/modules/settings-list/settings.ts +++ b/console/src/app/modules/settings-list/settings.ts @@ -228,8 +228,7 @@ export const ACTIONS: SidenavSetting = { i18nKey: 'SETTINGS.LIST.ACTIONS', groupI18nKey: 'SETTINGS.GROUPS.ACTIONS', requiredRoles: { - // todo: figure out roles - [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], + [PolicyComponentServiceType.ADMIN]: ['action.execution.write', 'action.target.write'], }, beta: true, }; @@ -239,8 +238,7 @@ export const ACTIONS_TARGETS: SidenavSetting = { i18nKey: 'SETTINGS.LIST.TARGETS', groupI18nKey: 'SETTINGS.GROUPS.ACTIONS', requiredRoles: { - // todo: figure out roles - [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], + [PolicyComponentServiceType.ADMIN]: ['action.execution.write', 'action.target.write'], }, beta: true, }; diff --git a/console/src/app/services/grpc-auth.service.ts b/console/src/app/services/grpc-auth.service.ts index 3967f1df06..198d048b6a 100644 --- a/console/src/app/services/grpc-auth.service.ts +++ b/console/src/app/services/grpc-auth.service.ts @@ -1,7 +1,18 @@ import { Injectable } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; import { OAuthService } from 'angular-oauth2-oidc'; -import { BehaviorSubject, combineLatestWith, EMPTY, mergeWith, NEVER, Observable, of, shareReplay, Subject } from 'rxjs'; +import { + BehaviorSubject, + combineLatestWith, + EMPTY, + identity, + mergeWith, + NEVER, + Observable, + of, + shareReplay, + Subject, +} from 'rxjs'; import { catchError, distinctUntilChanged, filter, finalize, map, startWith, switchMap, tap, timeout } from 'rxjs/operators'; import { @@ -326,7 +337,7 @@ export class GrpcAuthService { return new RegExp(reqRegexp).test(role); }); - const allCheck = requestedRoles.map(test).every((x) => !!x); + const allCheck = requestedRoles.map(test).every(identity); const oneCheck = requestedRoles.some(test); return requiresAll ? allCheck : oneCheck; From a05f7ce3fc864358ce3ef5cdd93386162eac5b25 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 30 Apr 2025 14:58:10 +0200 Subject: [PATCH 20/26] fix: correct handling of removed targets (#9824) # Which Problems Are Solved In Actions v2, if a target is removed, which is still used in an execution, the target is still listed when list executions. # How the Problems Are Solved Removed targets are now also removed from the executions. # Additional Changes To be sure the list executions include a check if the target is still existing. # Additional Context None Co-authored-by: Livio Spring --- internal/query/execution.go | 12 ++-- internal/query/execution_targets.sql | 22 ++++--- internal/query/execution_test.go | 71 +++++++++++++++++++-- internal/query/projection/execution.go | 25 ++++++++ internal/query/projection/execution_test.go | 30 +++++++++ 5 files changed, 141 insertions(+), 19 deletions(-) diff --git a/internal/query/execution.go b/internal/query/execution.go index 0a2a989918..4739a5839e 100644 --- a/internal/query/execution.go +++ b/internal/query/execution.go @@ -1,11 +1,13 @@ package query import ( + "cmp" "context" "database/sql" _ "embed" "encoding/json" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -301,13 +303,15 @@ func executionTargetsUnmarshal(data []byte) ([]*exec.Target, error) { } targets := make([]*exec.Target, len(executionTargets)) - // position starts with 1 - for _, item := range executionTargets { + slices.SortFunc(executionTargets, func(a, b *executionTarget) int { + return cmp.Compare(a.Position, b.Position) + }) + for i, item := range executionTargets { if item.Target != "" { - targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: item.Target} + targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: item.Target} } if item.Include != "" { - targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: item.Include} + targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: item.Include} } } return targets, nil diff --git a/internal/query/execution_targets.sql b/internal/query/execution_targets.sql index 32257f4a1f..a6e6dd6caa 100644 --- a/internal/query/execution_targets.sql +++ b/internal/query/execution_targets.sql @@ -1,11 +1,15 @@ -SELECT instance_id, - execution_id, +SELECT et.instance_id, + et.execution_id, JSONB_AGG( JSON_OBJECT( - 'position' : position, - 'include' : include, - 'target' : target_id - ) - ) as targets -FROM projections.executions1_targets -GROUP BY instance_id, execution_id \ No newline at end of file + 'position' : et.position, + 'include' : et.include, + 'target' : et.target_id + ) + ) as targets +FROM projections.executions1_targets AS et + INNER JOIN projections.targets2 AS t + ON et.instance_id = t.instance_id + AND et.target_id IS NOT NULL + AND et.target_id = t.id +GROUP BY et.instance_id, et.execution_id \ No newline at end of file diff --git a/internal/query/execution_test.go b/internal/query/execution_test.go index eaaac1e9ba..64f9a4849f 100644 --- a/internal/query/execution_test.go +++ b/internal/query/execution_test.go @@ -22,9 +22,10 @@ var ( ` COUNT(*) OVER ()` + ` FROM projections.executions1` + ` JOIN (` + - `SELECT instance_id, execution_id, JSONB_AGG( JSON_OBJECT( 'position' : position, 'include' : include, 'target' : target_id ) ) as targets` + - ` FROM projections.executions1_targets` + - ` GROUP BY instance_id, execution_id` + + `SELECT et.instance_id, et.execution_id, JSONB_AGG( JSON_OBJECT( 'position' : et.position, 'include' : et.include, 'target' : et.target_id ) ) as targets` + + ` FROM projections.executions1_targets AS et` + + ` INNER JOIN projections.targets2 AS t ON et.instance_id = t.instance_id AND et.target_id IS NOT NULL AND et.target_id = t.id` + + ` GROUP BY et.instance_id, et.execution_id` + `)` + ` AS execution_targets` + ` ON execution_targets.instance_id = projections.executions1.instance_id` + @@ -45,9 +46,10 @@ var ( ` execution_targets.targets` + ` FROM projections.executions1` + ` JOIN (` + - `SELECT instance_id, execution_id, JSONB_AGG( JSON_OBJECT( 'position' : position, 'include' : include, 'target' : target_id ) ) as targets` + - ` FROM projections.executions1_targets` + - ` GROUP BY instance_id, execution_id` + + `SELECT et.instance_id, et.execution_id, JSONB_AGG( JSON_OBJECT( 'position' : et.position, 'include' : et.include, 'target' : et.target_id ) ) as targets` + + ` FROM projections.executions1_targets AS et` + + ` INNER JOIN projections.targets2 AS t ON et.instance_id = t.instance_id AND et.target_id IS NOT NULL AND et.target_id = t.id` + + ` GROUP BY et.instance_id, et.execution_id` + `)` + ` AS execution_targets` + ` ON execution_targets.instance_id = projections.executions1.instance_id` + @@ -179,6 +181,63 @@ func Test_ExecutionPrepares(t *testing.T) { }, }, }, + { + name: "prepareExecutionsQuery multiple result, removed target, position missing", + prepare: prepareExecutionsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareExecutionsStmt), + prepareExecutionsCols, + [][]driver.Value{ + { + "ro", + "id-1", + testNow, + testNow, + []byte(`[{"position" : 1, "target" : "target"}, {"position" : 3, "include" : "include"}]`), + }, + { + "ro", + "id-2", + testNow, + testNow, + []byte(`[{"position" : 2, "target" : "target"}, {"position" : 1, "include" : "include"}]`), + }, + }, + ), + }, + object: &Executions{ + SearchResponse: SearchResponse{ + Count: 2, + }, + Executions: []*Execution{ + { + ObjectDetails: domain.ObjectDetails{ + ID: "id-1", + EventDate: testNow, + CreationDate: testNow, + ResourceOwner: "ro", + }, + Targets: []*exec.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, + }, + { + ObjectDetails: domain.ObjectDetails{ + ID: "id-2", + EventDate: testNow, + CreationDate: testNow, + ResourceOwner: "ro", + }, + Targets: []*exec.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + }, + }, + }, + }, { name: "prepareExecutionsQuery sql err", prepare: prepareExecutionsQuery, diff --git a/internal/query/projection/execution.go b/internal/query/projection/execution.go index 9001fcd3ba..1bd7f2e7f5 100644 --- a/internal/query/projection/execution.go +++ b/internal/query/projection/execution.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/handler/v2" exec "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/target" ) const ( @@ -78,6 +79,15 @@ func (p *executionProjection) Reducers() []handler.AggregateReducer { }, }, }, + { + Aggregate: target.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: target.RemovedEventType, + Reduce: p.reduceTargetRemoved, + }, + }, + }, { Aggregate: instance.AggregateType, EventReducers: []handler.EventReducer{ @@ -152,6 +162,21 @@ func (p *executionProjection) reduceExecutionSet(event eventstore.Event) (*handl return handler.NewMultiStatement(e, stmts...), nil } +func (p *executionProjection) reduceTargetRemoved(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*target.RemovedEvent](event) + if err != nil { + return nil, err + } + return handler.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(ExecutionTargetInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCond(ExecutionTargetTargetIDCol, e.Aggregate().ID), + }, + handler.WithTableSuffix(ExecutionTargetSuffix), + ), nil +} + func (p *executionProjection) reduceExecutionRemoved(event eventstore.Event) (*handler.Statement, error) { e, err := assertEvent[*exec.RemovedEvent](event) if err != nil { diff --git a/internal/query/projection/execution_test.go b/internal/query/projection/execution_test.go index 27d6e89258..aecae6905a 100644 --- a/internal/query/projection/execution_test.go +++ b/internal/query/projection/execution_test.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/handler/v2" exec "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/target" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -79,6 +80,35 @@ func TestExecutionProjection_reduces(t *testing.T) { }, }, }, + { + name: "reduceTargetRemoved", + args: args{ + event: getEvent( + testEvent( + target.RemovedEventType, + target.AggregateType, + []byte(`{}`), + ), + eventstore.GenericEventMapper[target.RemovedEvent], + ), + }, + reduce: (&executionProjection{}).reduceTargetRemoved, + want: wantReduce{ + aggregateType: eventstore.AggregateType("target"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.executions1_targets WHERE (instance_id = $1) AND (target_id = $2)", + expectedArgs: []interface{}{ + "instance-id", + "agg-id", + }, + }, + }, + }, + }, + }, { name: "reduceExecutionRemoved", args: args{ From 02acc932425c9b3519b8ad6c1dc334ad134319c5 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 30 Apr 2025 15:20:39 +0200 Subject: [PATCH 21/26] fix: Improve Actions V2 translations (#9826) # Which Problems Are Solved The translation for event was not loaded correctly. ![grafik](https://github.com/user-attachments/assets/3fa8d72f-f55a-44b7-997d-0f0976f66b85) # How the Problems Are Solved Correct translations to have the correct key. # Additional Changes Improved the translation for all events. --- .../actions-two-add-action-condition.component.html | 2 +- console/src/assets/i18n/bg.json | 3 ++- console/src/assets/i18n/cs.json | 3 ++- console/src/assets/i18n/de.json | 3 ++- console/src/assets/i18n/en.json | 3 ++- console/src/assets/i18n/es.json | 3 ++- console/src/assets/i18n/fr.json | 3 ++- console/src/assets/i18n/hu.json | 3 ++- console/src/assets/i18n/id.json | 3 ++- console/src/assets/i18n/it.json | 3 ++- console/src/assets/i18n/ja.json | 3 ++- console/src/assets/i18n/ko.json | 3 ++- console/src/assets/i18n/mk.json | 3 ++- console/src/assets/i18n/nl.json | 3 ++- console/src/assets/i18n/pl.json | 3 ++- console/src/assets/i18n/pt.json | 3 ++- console/src/assets/i18n/ro.json | 3 ++- console/src/assets/i18n/ru.json | 3 ++- console/src/assets/i18n/sv.json | 3 ++- console/src/assets/i18n/zh.json | 3 ++- 20 files changed, 39 insertions(+), 20 deletions(-) diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html index 401e5e521d..f0248f45a2 100644 --- a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html @@ -84,7 +84,7 @@
{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.TITLE' | translate }} {{ - 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.DESCRIPTION' | translate + 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL_EVENTS' | translate }}
diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 30d9c53763..b98204a917 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -536,7 +536,7 @@ "TYPES": { "request": "Заявка", "response": "Отговор", - "events": "Събития", + "event": "Събития", "function": "Функция" }, "DIALOG": { @@ -567,6 +567,7 @@ "TITLE": "Всички", "DESCRIPTION": "Изберете това, ако искате да изпълните действието си при всяка заявка" }, + "ALL_EVENTS": "Изберете това, ако искате действието да се изпълнява при всяко събитие", "SELECT_SERVICE": { "TITLE": "Избор на услуга", "DESCRIPTION": "Изберете услуга на Zitadel за вашето действие." diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index ee43e86822..390c5dcdbd 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -537,7 +537,7 @@ "TYPES": { "request": "Požadavek", "response": "Odpověď", - "events": "Události", + "event": "Události", "function": "Funkce" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "Všechny", "DESCRIPTION": "Vyberte tuto možnost, pokud chcete spustit akci pro každý požadavek" }, + "ALL_EVENTS": "Vyberte toto, pokud chcete spustit akci při každé události", "SELECT_SERVICE": { "TITLE": "Vybrat službu", "DESCRIPTION": "Vyberte službu Zitadel pro svou akci." diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 3993674992..e73c883bd2 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -537,7 +537,7 @@ "TYPES": { "request": "Anfrage", "response": "Antwort", - "events": "Ereignisse", + "event": "Ereignisse", "function": "Funktion" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "Alle", "DESCRIPTION": "Wählen Sie dies aus, wenn Sie Ihre Aktion bei jeder Anfrage ausführen möchten" }, + "ALL_EVENTS": "Wähle dies aus, wenn du deine Aktion bei jedem Ereignis ausführen möchtest", "SELECT_SERVICE": { "TITLE": "Dienst auswählen", "DESCRIPTION": "Wählen Sie einen Zitadel-Dienst für Ihre Aktion aus." diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index fd81bfd353..5e2cc3f4c9 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -537,7 +537,7 @@ "TYPES": { "request": "Request", "response": "Response", - "events": "Events", + "event": "Events", "function": "Function" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "All", "DESCRIPTION": "Select this if you want to run your action on every request" }, + "ALL_EVENTS": "Select this if you want to run your action on every event", "SELECT_SERVICE": { "TITLE": "Select Service", "DESCRIPTION": "Choose a Zitadel Service for you action." diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index aec024eacb..198bb3ca8b 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -537,7 +537,7 @@ "TYPES": { "request": "Solicitud", "response": "Respuesta", - "events": "Eventos", + "event": "Eventos", "function": "Función" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "Todas", "DESCRIPTION": "Selecciona esto si quieres ejecutar tu acción en cada solicitud" }, + "ALL_EVENTS": "Selecciona esto si quieres ejecutar tu acción en cada evento", "SELECT_SERVICE": { "TITLE": "Seleccionar servicio", "DESCRIPTION": "Elige un servicio de Zitadel para tu acción." diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 05e34ad846..0d66c4193e 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -537,7 +537,7 @@ "TYPES": { "request": "Requête", "response": "Réponse", - "events": "Événements", + "event": "Événements", "function": "Fonction" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "Tous", "DESCRIPTION": "Sélectionnez ceci si vous souhaitez exécuter votre action sur chaque requête" }, + "ALL_EVENTS": "Sélectionnez ceci si vous souhaitez exécuter votre action à chaque événement", "SELECT_SERVICE": { "TITLE": "Sélectionner un service", "DESCRIPTION": "Choisissez un service Zitadel pour votre action." diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json index d46bc96153..96d1fe16df 100644 --- a/console/src/assets/i18n/hu.json +++ b/console/src/assets/i18n/hu.json @@ -537,7 +537,7 @@ "TYPES": { "request": "Kérés", "response": "Válasz", - "events": "Események", + "event": "Események", "function": "Függvény" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "Összes", "DESCRIPTION": "Válassza ezt, ha minden kérésnél futtatni szeretné a műveletet" }, + "ALL_EVENTS": "Válaszd ezt, ha minden eseménynél futtatni szeretnéd a műveletet", "SELECT_SERVICE": { "TITLE": "Szolgáltatás kiválasztása", "DESCRIPTION": "Válasszon egy Zitadel szolgáltatást a művelethez." diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json index f8831a224d..ca788a9467 100644 --- a/console/src/assets/i18n/id.json +++ b/console/src/assets/i18n/id.json @@ -504,7 +504,7 @@ "TYPES": { "request": "Permintaan", "response": "Respons", - "events": "Peristiwa", + "event": "Peristiwa", "function": "Fungsi" }, "DIALOG": { @@ -535,6 +535,7 @@ "TITLE": "Semua", "DESCRIPTION": "Pilih ini jika Anda ingin menjalankan tindakan Anda pada setiap permintaan" }, + "ALL_EVENTS": "Pilih ini jika Anda ingin menjalankan aksi Anda pada setiap peristiwa", "SELECT_SERVICE": { "TITLE": "Pilih Layanan", "DESCRIPTION": "Pilih Layanan Zitadel untuk tindakan Anda." diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 5dad683bca..60266bdac5 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -536,7 +536,7 @@ "TYPES": { "request": "Richiesta", "response": "Risposta", - "events": "Eventi", + "event": "Eventi", "function": "Funzione" }, "DIALOG": { @@ -567,6 +567,7 @@ "TITLE": "Tutte", "DESCRIPTION": "Seleziona questa opzione se vuoi eseguire la tua azione su ogni richiesta" }, + "ALL_EVENTS": "Seleziona questo se vuoi eseguire la tua azione a ogni evento", "SELECT_SERVICE": { "TITLE": "Seleziona servizio", "DESCRIPTION": "Scegli un servizio Zitadel per la tua azione." diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index f09dfeb564..288d491ce7 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -537,7 +537,7 @@ "TYPES": { "request": "リクエスト", "response": "レスポンス", - "events": "イベント", + "event": "イベント", "function": "関数" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "すべて", "DESCRIPTION": "すべてのリクエストでアクションを実行する場合は、これを選択します" }, + "ALL_EVENTS": "すべてのイベントでアクションを実行する場合はこれを選択してください", "SELECT_SERVICE": { "TITLE": "サービスを選択", "DESCRIPTION": "アクションのZitadelサービスを選択します。" diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json index 304f52e127..437c43a3a1 100644 --- a/console/src/assets/i18n/ko.json +++ b/console/src/assets/i18n/ko.json @@ -537,7 +537,7 @@ "TYPES": { "request": "요청", "response": "응답", - "events": "이벤트", + "event": "이벤트", "function": "함수" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "모두", "DESCRIPTION": "모든 요청에서 작업을 실행하려면 이것을 선택하십시오." }, + "ALL_EVENTS": "모든 이벤트에서 작업을 실행하려면 이 항목을 선택하세요", "SELECT_SERVICE": { "TITLE": "서비스 선택", "DESCRIPTION": "작업에 대한 Zitadel 서비스를 선택하십시오." diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 1ab4dce534..2e62723939 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -537,7 +537,7 @@ "TYPES": { "request": "Барање", "response": "Одговор", - "events": "Настани", + "event": "Настани", "function": "Функција" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "Сите", "DESCRIPTION": "Изберете го ова ако сакате да ја извршите вашата акција на секое барање" }, + "ALL_EVENTS": "Изберете го ова ако сакате вашата акција да се извршува на секој настан", "SELECT_SERVICE": { "TITLE": "Изберете услуга", "DESCRIPTION": "Изберете Zitadel услуга за вашата акција." diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index a08f62ed20..7e549f64ba 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -537,7 +537,7 @@ "TYPES": { "request": "Verzoek", "response": "Reactie", - "events": "Gebeurtenissen", + "event": "Gebeurtenissen", "function": "Functie" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "Alle", "DESCRIPTION": "Selecteer dit als u uw actie bij elk verzoek wilt uitvoeren" }, + "ALL_EVENTS": "Selecteer dit als je je actie bij elk evenement wilt uitvoeren", "SELECT_SERVICE": { "TITLE": "Service selecteren", "DESCRIPTION": "Kies een Zitadel-service voor uw actie." diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 3902378af6..2f18c343f7 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -536,7 +536,7 @@ "TYPES": { "request": "Żądanie", "response": "Odpowiedź", - "events": "Zdarzenia", + "event": "Zdarzenia", "function": "Funkcja" }, "DIALOG": { @@ -567,6 +567,7 @@ "TITLE": "Wszystkie", "DESCRIPTION": "Wybierz tę opcję, jeśli chcesz uruchomić akcję dla każdego żądania" }, + "ALL_EVENTS": "Wybierz to, jeśli chcesz uruchamiać swoją akcję przy każdym zdarzeniu", "SELECT_SERVICE": { "TITLE": "Wybierz usługę", "DESCRIPTION": "Wybierz usługę Zitadel dla swojej akcji." diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index 016785f2c8..08181f6ead 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -537,7 +537,7 @@ "TYPES": { "request": "Solicitação", "response": "Resposta", - "events": "Eventos", + "event": "Eventos", "function": "Função" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "Todas", "DESCRIPTION": "Selecione isso se você quiser executar sua ação em cada solicitação" }, + "ALL_EVENTS": "Selecione isto se quiser executar sua ação em cada evento", "SELECT_SERVICE": { "TITLE": "Selecionar Serviço", "DESCRIPTION": "Escolha um Serviço Zitadel para sua ação." diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json index 4ad511c466..b07897f316 100644 --- a/console/src/assets/i18n/ro.json +++ b/console/src/assets/i18n/ro.json @@ -537,7 +537,7 @@ "TYPES": { "request": "Cerere", "response": "Răspuns", - "events": "Evenimente", + "event": "Evenimente", "function": "Funcție" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "Toate", "DESCRIPTION": "Selectați aceasta dacă doriți să rulați acțiunea la fiecare cerere" }, + "ALL_EVENTS": "Selectează aceasta dacă vrei să rulezi acțiunea ta la fiecare eveniment", "SELECT_SERVICE": { "TITLE": "Selectați Serviciul", "DESCRIPTION": "Alegeți un Serviciu Zitadel pentru acțiunea dvs." diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index 43bc266be0..c6ef31499e 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -537,7 +537,7 @@ "TYPES": { "request": "Запрос", "response": "Ответ", - "events": "События", + "event": "События", "function": "Функция" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "Все", "DESCRIPTION": "Выберите это, если вы хотите запустить свое действие при каждом запросе" }, + "ALL_EVENTS": "Выберите это, если хотите выполнять действие при каждом событии", "SELECT_SERVICE": { "TITLE": "Выбрать службу", "DESCRIPTION": "Выберите службу Zitadel для вашего действия." diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index 00b7854603..c356e635e0 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -537,7 +537,7 @@ "TYPES": { "request": "Förfrågan", "response": "Svar", - "events": "Händelser", + "event": "Händelser", "function": "Funktion" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "Alla", "DESCRIPTION": "Välj detta om du vill köra din åtgärd på varje förfrågan" }, + "ALL_EVENTS": "Välj detta om du vill köra din åtgärd vid varje händelse", "SELECT_SERVICE": { "TITLE": "Välj tjänst", "DESCRIPTION": "Välj en Zitadel-tjänst för din åtgärd." diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 496b3d528e..8be3316b0b 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -537,7 +537,7 @@ "TYPES": { "request": "请求", "response": "响应", - "events": "事件", + "event": "事件", "function": "函数" }, "DIALOG": { @@ -568,6 +568,7 @@ "TITLE": "全部", "DESCRIPTION": "如果您希望在每个请求上运行您的操作,请选择此项" }, + "ALL_EVENTS": "如果您想在每个事件上运行操作,请选择此项", "SELECT_SERVICE": { "TITLE": "选择服务", "DESCRIPTION": "为您的操作选择一个 Zitadel 服务。" From 74ace1aec31eb6085c1b871c7465524f5f9d13cf Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Thu, 1 May 2025 07:41:57 +0200 Subject: [PATCH 22/26] fix(actions): default sorting column to creation date (#9795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved The sorting column of action targets and executions defaults to the ID column instead of the creation date column. This is only relevant, if the sorting column is explicitly passed as unspecified. If the sorting column is not passed, it correctly defaults to the creation date. ```bash # ❌ Sorts by ID grpcurl -plaintext -H "Authorization: Bearer ${ZITADEL_ACCESS_TOKEN}" -d '{"sortingColumn": "TARGET_FIELD_NAME_UNSPECIFIED"}' localhost:8080 zitadel.action.v2beta.ActionService.ListTargets # ❌ Sorts by ID grpcurl -plaintext -H "Authorization: Bearer ${ZITADEL_ACCESS_TOKEN}" -d '{"sortingColumn": 0}' localhost:8080 zitadel.action.v2beta.ActionService.ListTargets # ✅ Sorts by creation date grpcurl -plaintext -H "Authorization: Bearer ${ZITADEL_ACCESS_TOKEN}" localhost:8080 zitadel.action.v2beta.ActionService.ListTargets ``` # How the Problems Are Solved `action.TargetFieldName_TARGET_FIELD_NAME_UNSPECIFIED` maps to the sorting column `query.TargetColumnCreationDate`. # Additional Context As IDs are also generated in ascending, like creation dates, the the bug probably only causes unexpected behavior for cases, where the ID is specified during target or execution creation. This is currently not supported, so this bug probably has no impact at all. It doesn't need to be backported. Found during implementation of #9763 Co-authored-by: Livio Spring --- internal/api/grpc/action/v2beta/query.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/api/grpc/action/v2beta/query.go b/internal/api/grpc/action/v2beta/query.go index 66bafa4e7d..1dbe80a8f7 100644 --- a/internal/api/grpc/action/v2beta/query.go +++ b/internal/api/grpc/action/v2beta/query.go @@ -164,7 +164,7 @@ func targetFieldNameToSortingColumn(field *action.TargetFieldName) query.Column } switch *field { case action.TargetFieldName_TARGET_FIELD_NAME_UNSPECIFIED: - return query.TargetColumnID + return query.TargetColumnCreationDate case action.TargetFieldName_TARGET_FIELD_NAME_ID: return query.TargetColumnID case action.TargetFieldName_TARGET_FIELD_NAME_CREATED_DATE: @@ -193,7 +193,7 @@ func executionFieldNameToSortingColumn(field *action.ExecutionFieldName) query.C } switch *field { case action.ExecutionFieldName_EXECUTION_FIELD_NAME_UNSPECIFIED: - return query.ExecutionColumnID + return query.ExecutionColumnCreationDate case action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID: return query.ExecutionColumnID case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CREATED_DATE: From bb56b362a755b5e07dfd364db59e1c02cd21690e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 2 May 2025 13:40:22 +0200 Subject: [PATCH 23/26] perf(eventstore): add instance position index (#9837) # Which Problems Are Solved Some projection queries took a long time to run. It seems that 1 or more queries couldn't make proper use of the `es_projection` index. This might be because of a specific complexity aggregate_type and event_type arguments, making the index unfeasible for postgres. # How the Problems Are Solved Following the index recommendation, add and index that covers just instance_id and position. # Additional Changes - none # Additional Context - Related to https://github.com/zitadel/zitadel/issues/9832 --- cmd/setup/54.go | 27 +++++++++++++++++++++++++++ cmd/setup/54.sql | 1 + 2 files changed, 28 insertions(+) create mode 100644 cmd/setup/54.go create mode 100644 cmd/setup/54.sql diff --git a/cmd/setup/54.go b/cmd/setup/54.go new file mode 100644 index 0000000000..3dd2f60abe --- /dev/null +++ b/cmd/setup/54.go @@ -0,0 +1,27 @@ +package setup + +import ( + "context" + _ "embed" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 54.sql + instancePositionIndex string +) + +type InstancePositionIndex struct { + dbClient *database.DB +} + +func (mig *InstancePositionIndex) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, instancePositionIndex) + return err +} + +func (mig *InstancePositionIndex) String() string { + return "54_instance_position_index" +} diff --git a/cmd/setup/54.sql b/cmd/setup/54.sql new file mode 100644 index 0000000000..1dca8c7575 --- /dev/null +++ b/cmd/setup/54.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS es_instance_position ON eventstore.events2 (instance_id, position); From b1e60e7398d677f08b06fd7715227f70b7ca1162 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 2 May 2025 13:44:24 +0200 Subject: [PATCH 24/26] Merge commit from fork * fix: prevent intent token reuse and add expiry * fix duplicate * fix expiration --- cmd/defaults.yaml | 3 + .../v2/integration_test/session_test.go | 80 +++++++++++- .../v2beta/integration_test/session_test.go | 80 +++++++++++- .../user/v2/integration_test/user_test.go | 52 ++++++-- internal/api/grpc/user/v2/intent.go | 17 ++- .../user/v2beta/integration_test/user_test.go | 52 ++++++-- internal/api/grpc/user/v2beta/user.go | 12 +- internal/api/idp/idp.go | 2 +- internal/api/idp/idp_test.go | 38 +++--- internal/command/command.go | 2 + internal/command/idp_intent.go | 20 ++- internal/command/idp_intent_model.go | 29 ++++- internal/command/idp_intent_test.go | 56 ++++++--- internal/command/session.go | 51 ++++---- internal/command/session_test.go | 114 +++++++++++++++++- .../config/systemdefaults/system_defaults.go | 19 +-- internal/domain/idp.go | 1 + internal/idp/providers/apple/session.go | 2 + internal/idp/providers/azuread/session.go | 10 ++ internal/idp/providers/jwt/session.go | 7 ++ internal/idp/providers/ldap/session.go | 4 + internal/idp/providers/oauth/session.go | 8 ++ internal/idp/providers/oidc/session.go | 8 ++ internal/idp/providers/saml/session.go | 8 ++ internal/idp/session.go | 2 + internal/integration/client.go | 17 +++ internal/integration/sink/server.go | 42 +++++-- internal/repository/idpintent/eventstore.go | 1 + internal/repository/idpintent/intent.go | 40 ++++++ internal/static/i18n/bg.yaml | 1 + internal/static/i18n/cs.yaml | 1 + internal/static/i18n/de.yaml | 1 + internal/static/i18n/en.yaml | 1 + internal/static/i18n/es.yaml | 1 + internal/static/i18n/fr.yaml | 1 + internal/static/i18n/hu.yaml | 1 + internal/static/i18n/id.yaml | 1 + internal/static/i18n/it.yaml | 1 + internal/static/i18n/ja.yaml | 1 + internal/static/i18n/ko.yaml | 1 + internal/static/i18n/mk.yaml | 1 + internal/static/i18n/nl.yaml | 1 + internal/static/i18n/pl.yaml | 1 + internal/static/i18n/pt.yaml | 1 + internal/static/i18n/ro.yaml | 1 + internal/static/i18n/ru.yaml | 1 + internal/static/i18n/sv.yaml | 1 + internal/static/i18n/zh.yaml | 1 + 48 files changed, 673 insertions(+), 123 deletions(-) diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 6ab01ab35b..0d71b4d817 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -735,6 +735,9 @@ SystemDefaults: DefaultQueryLimit: 100 # ZITADEL_SYSTEMDEFAULTS_DEFAULTQUERYLIMIT # MaxQueryLimit limits the number of items that can be queried in a single v3 API search request with explicitly passing a limit. MaxQueryLimit: 1000 # ZITADEL_SYSTEMDEFAULTS_MAXQUERYLIMIT + # The maximum duration of the IDP intent lifetime after which the IDP intent expires and can not be retrieved or used anymore. + # Note that this time is measured only after the IdP intent was successful and not after the IDP intent was created. + MaxIdPIntentLifetime: 1h # ZITADEL_SYSTEMDEFAULTS_MAXIDPINTENTLIFETIME Actions: HTTP: diff --git a/internal/api/grpc/session/v2/integration_test/session_test.go b/internal/api/grpc/session/v2/integration_test/session_test.go index b9a060c749..0982a56121 100644 --- a/internal/api/grpc/session/v2/integration_test/session_test.go +++ b/internal/api/grpc/session/v2/integration_test/session_test.go @@ -354,7 +354,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { require.NoError(t, err) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), @@ -372,7 +372,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ @@ -396,7 +396,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { // successful intent without known / linked user idpUserID := "id" - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, "") + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, "", time.Now().Add(time.Hour)) // link the user (with info from intent) Instance.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId()) @@ -447,6 +447,80 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { require.Error(t, err) } +func TestServer_CreateSession_reuseIntent(t *testing.T) { + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) + require.NoError(t, err) + updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor) + + // the reuse of the intent token is not allowed, not even on the same session + session2, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.Error(t, err) + _ = session2 +} + +func TestServer_CreateSession_expiredIntent(t *testing.T) { + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Second)) + require.NoError(t, err) + + // wait for the intent to expire + time.Sleep(2 * time.Second) + + _, err = Client.SetSession(LoginCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.Error(t, err) +} + func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) { resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{ UserId: userID, diff --git a/internal/api/grpc/session/v2beta/integration_test/session_test.go b/internal/api/grpc/session/v2beta/integration_test/session_test.go index d0fc1179ef..4c189e0f80 100644 --- a/internal/api/grpc/session/v2beta/integration_test/session_test.go +++ b/internal/api/grpc/session/v2beta/integration_test/session_test.go @@ -354,7 +354,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { require.NoError(t, err) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), @@ -372,7 +372,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ @@ -396,7 +396,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { // successful intent without known / linked user idpUserID := "id" - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) // link the user (with info from intent) @@ -448,6 +448,80 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { require.Error(t, err) } +func TestServer_CreateSession_reuseIntent(t *testing.T) { + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(IAMOwnerCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) + require.NoError(t, err) + updateResp, err := Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor) + + // the reuse of the intent token is not allowed, not even on the same session + session2, err := Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.Error(t, err) + _ = session2 +} + +func TestServer_CreateSession_expiredIntent(t *testing.T) { + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(IAMOwnerCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Second)) + require.NoError(t, err) + + // wait for the intent to expire + time.Sleep(2 * time.Second) + + _, err = Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.Error(t, err) +} + func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) { resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{ UserId: userID, diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index bf396fd25d..70e670bacc 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -2121,22 +2121,36 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) require.NoError(t, err) intentID := authURL.Query().Get("state") + expiry := time.Now().Add(1 * time.Hour) + expiryFormatted := expiry.Round(time.Millisecond).UTC().Format("2006-01-02T15:04:05.999Z07:00") - successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "") + intentUser := Instance.CreateHumanUser(IamCTX) + _, err = Instance.CreateUserIDPlink(IamCTX, intentUser.GetUserId(), "idpUserID", oauthIdpID, "username") require.NoError(t, err) - successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user") + + successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry) require.NoError(t, err) - oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "") + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry) require.NoError(t, err) - oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user") + successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second)) + require.NoError(t, err) + // make sure the intent is expired + time.Sleep(2 * time.Second) + successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry) + require.NoError(t, err) + // make sure the intent is consumed + Instance.CreateIntentSession(t, IamCTX, intentUser.GetUserId(), successfulConsumedID, consumedToken) + oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "", expiry) + require.NoError(t, err) + oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user", expiry) require.NoError(t, err) ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "") require.NoError(t, err) ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user") require.NoError(t, err) - samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "") + samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry) require.NoError(t, err) - samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user") + samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry) require.NoError(t, err) type args struct { ctx context.Context @@ -2260,6 +2274,28 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful expired intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulExpiredID, + IdpIntentToken: expiredToken, + }, + }, + wantErr: true, + }, + { + name: "retrieve successful consumed intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulConsumedID, + IdpIntentToken: consumedToken, + }, + }, + wantErr: true, + }, { name: "retrieve successful oidc intent", args: args{ @@ -2469,7 +2505,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, IdpId: samlIdpID, @@ -2518,7 +2554,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, IdpId: samlIdpID, diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go index 06966edb35..8043a9bdae 100644 --- a/internal/api/grpc/user/v2/intent.go +++ b/internal/api/grpc/user/v2/intent.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "time" oidc_pkg "github.com/zitadel/oidc/v3/pkg/oidc" "google.golang.org/protobuf/types/known/structpb" @@ -71,14 +72,14 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti if err != nil { return nil, err } - externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) + externalUser, userID, session, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) if err != nil { if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil { return nil, err } return nil, err } - token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes) + token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, session) if err != nil { return nil, err } @@ -116,7 +117,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse return "", nil } -func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { +func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, *ldap.Session, error) { provider, err := s.command.GetProvider(ctx, idpID, "", "") if err != nil { return nil, "", nil, err @@ -137,12 +138,7 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string if err != nil { return nil, "", nil, err } - - attributes := make(map[string][]string, 0) - for _, item := range session.Entry.Attributes { - attributes[item.Name] = item.Values - } - return externalUser, userID, attributes, nil + return externalUser, userID, session, nil } func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { @@ -156,6 +152,9 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R if intent.State != domain.IDPIntentStateSucceeded { return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") } + if time.Now().After(intent.ExpiresAt()) { + return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-SAf42", "Errors.Intent.Expired") + } idpIntent, err := idpIntentToIDPIntentPb(intent, s.idpAlg) if err != nil { return nil, err diff --git a/internal/api/grpc/user/v2beta/integration_test/user_test.go b/internal/api/grpc/user/v2beta/integration_test/user_test.go index a81de58761..a5a1309d1a 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -2153,22 +2153,36 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) require.NoError(t, err) intentID := authURL.Query().Get("state") + expiry := time.Now().Add(1 * time.Hour) + expiryFormatted := expiry.Round(time.Millisecond).UTC().Format("2006-01-02T15:04:05.999Z07:00") - successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "") + intentUser := Instance.CreateHumanUser(IamCTX) + _, err = Instance.CreateUserIDPlink(IamCTX, intentUser.GetUserId(), "idpUserID", oauthIdpID, "username") require.NoError(t, err) - successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user") + + successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry) require.NoError(t, err) - oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "") + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry) require.NoError(t, err) - oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user") + successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second)) + require.NoError(t, err) + // make sure the intent is expired + time.Sleep(2 * time.Second) + successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry) + require.NoError(t, err) + // make sure the intent is consumed + Instance.CreateIntentSession(t, IamCTX, intentUser.GetUserId(), successfulConsumedID, consumedToken) + oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "", expiry) + require.NoError(t, err) + oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user", expiry) require.NoError(t, err) ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "") require.NoError(t, err) ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user") require.NoError(t, err) - samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "") + samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry) require.NoError(t, err) - samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user") + samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry) require.NoError(t, err) type args struct { ctx context.Context @@ -2281,6 +2295,28 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful expired intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulExpiredID, + IdpIntentToken: expiredToken, + }, + }, + wantErr: true, + }, + { + name: "retrieve successful consumed intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulConsumedID, + IdpIntentToken: consumedToken, + }, + }, + wantErr: true, + }, { name: "retrieve successful oidc intent", args: args{ @@ -2466,7 +2502,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, IdpId: samlIdpID, @@ -2504,7 +2540,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, IdpId: samlIdpID, diff --git a/internal/api/grpc/user/v2beta/user.go b/internal/api/grpc/user/v2beta/user.go index cf6dfa6304..93afbde0aa 100644 --- a/internal/api/grpc/user/v2beta/user.go +++ b/internal/api/grpc/user/v2beta/user.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "time" "golang.org/x/text/language" "google.golang.org/protobuf/types/known/structpb" @@ -399,14 +400,14 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti if err != nil { return nil, err } - externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) + externalUser, userID, session, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) if err != nil { if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil { return nil, err } return nil, err } - token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes) + token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, session) if err != nil { return nil, err } @@ -444,7 +445,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse return "", nil } -func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { +func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, *ldap.Session, error) { provider, err := s.command.GetProvider(ctx, idpID, "", "") if err != nil { return nil, "", nil, err @@ -470,7 +471,7 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string for _, item := range session.Entry.Attributes { attributes[item.Name] = item.Values } - return externalUser, userID, attributes, nil + return externalUser, userID, session, nil } func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { @@ -484,6 +485,9 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R if intent.State != domain.IDPIntentStateSucceeded { return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") } + if time.Now().After(intent.ExpiresAt()) { + return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-Afb2s", "Errors.Intent.Expired") + } return idpIntentToIDPIntentPb(intent, s.idpAlg) } diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index c3e9586a59..ebf904a395 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -287,7 +287,7 @@ func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) { userID, err := h.checkExternalUser(ctx, intent.IDPID, idpUser.GetID()) logging.WithFields("intent", intent.AggregateID).OnError(err).Error("could not check if idp user already exists") - token, err := h.commands.SucceedSAMLIDPIntent(ctx, intent, idpUser, userID, session.Assertion) + token, err := h.commands.SucceedSAMLIDPIntent(ctx, intent, idpUser, userID, session) if err != nil { redirectToFailureURLErr(w, r, intent, zerrors.ThrowInternal(err, "IDP-JdD3g", "Errors.Intent.TokenCreationFailed")) return diff --git a/internal/api/idp/idp_test.go b/internal/api/idp/idp_test.go index 6804a035af..2f64f598a9 100644 --- a/internal/api/idp/idp_test.go +++ b/internal/api/idp/idp_test.go @@ -4,6 +4,7 @@ import ( "net/http/httptest" "net/url" "testing" + "time" "github.com/stretchr/testify/assert" @@ -14,11 +15,12 @@ import ( func Test_redirectToSuccessURL(t *testing.T) { type args struct { - id string - userID string - token string - failureURL string - successURL string + id string + userID string + token string + failureURL string + successURL string + maxIdPIntentLifetime time.Duration } type res struct { want string @@ -59,7 +61,7 @@ func Test_redirectToSuccessURL(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) resp := httptest.NewRecorder() - wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id) + wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime) wm.FailureURL, _ = url.Parse(tt.args.failureURL) wm.SuccessURL, _ = url.Parse(tt.args.successURL) @@ -71,11 +73,12 @@ func Test_redirectToSuccessURL(t *testing.T) { func Test_redirectToFailureURL(t *testing.T) { type args struct { - id string - failureURL string - successURL string - err string - desc string + id string + failureURL string + successURL string + err string + desc string + maxIdPIntentLifetime time.Duration } type res struct { want string @@ -115,7 +118,7 @@ func Test_redirectToFailureURL(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) resp := httptest.NewRecorder() - wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id) + wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime) wm.FailureURL, _ = url.Parse(tt.args.failureURL) wm.SuccessURL, _ = url.Parse(tt.args.successURL) @@ -127,10 +130,11 @@ func Test_redirectToFailureURL(t *testing.T) { func Test_redirectToFailureURLErr(t *testing.T) { type args struct { - id string - failureURL string - successURL string - err error + id string + failureURL string + successURL string + err error + maxIdPIntentLifetime time.Duration } type res struct { want string @@ -158,7 +162,7 @@ func Test_redirectToFailureURLErr(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) resp := httptest.NewRecorder() - wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id) + wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime) wm.FailureURL, _ = url.Parse(tt.args.failureURL) wm.SuccessURL, _ = url.Parse(tt.args.successURL) diff --git a/internal/command/command.go b/internal/command/command.go index b0e67ad52e..64b7b53b67 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -81,6 +81,7 @@ type Commands struct { publicKeyLifetime time.Duration certificateLifetime time.Duration defaultSecretGenerators *SecretGenerators + maxIdPIntentLifetime time.Duration samlCertificateAndKeyGenerator func(id string) ([]byte, []byte, error) webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error) @@ -152,6 +153,7 @@ func StartCommands( privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime, publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime, certificateLifetime: defaults.KeyConfig.CertificateLifetime, + maxIdPIntentLifetime: defaults.MaxIdPIntentLifetime, idpConfigEncryption: idpConfigEncryption, smtpEncryption: smtpEncryption, smsEncryption: smsEncryption, diff --git a/internal/command/idp_intent.go b/internal/command/idp_intent.go index 3cd9991679..9690117edd 100644 --- a/internal/command/idp_intent.go +++ b/internal/command/idp_intent.go @@ -7,7 +7,6 @@ import ( "encoding/xml" "net/url" - "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -19,8 +18,10 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/apple" "github.com/zitadel/zitadel/internal/idp/providers/azuread" "github.com/zitadel/zitadel/internal/idp/providers/jwt" + "github.com/zitadel/zitadel/internal/idp/providers/ldap" "github.com/zitadel/zitadel/internal/idp/providers/oauth" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" + "github.com/zitadel/zitadel/internal/idp/providers/saml" "github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -68,7 +69,7 @@ func (c *Commands) CreateIntent(ctx context.Context, intentID, idpID, successURL return nil, nil, err } } - writeModel := NewIDPIntentWriteModel(intentID, resourceOwner) + writeModel := NewIDPIntentWriteModel(intentID, resourceOwner, c.maxIdPIntentLifetime) //nolint: staticcheck cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL, idpArguments)) @@ -180,6 +181,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr userID, accessToken, idToken, + idpSession.ExpiresAt(), ) err = c.pushAppendAndReduce(ctx, writeModel, cmd) if err != nil { @@ -188,7 +190,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr return token, nil } -func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, assertion *saml.Assertion) (string, error) { +func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, session *saml.Session) (string, error) { token, err := c.generateIntentToken(writeModel.AggregateID) if err != nil { return "", err @@ -197,7 +199,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte if err != nil { return "", err } - assertionData, err := xml.Marshal(assertion) + assertionData, err := xml.Marshal(session.Assertion) if err != nil { return "", err } @@ -213,6 +215,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte idpUser.GetPreferredUsername(), userID, assertionEnc, + session.ExpiresAt(), ) err = c.pushAppendAndReduce(ctx, writeModel, cmd) if err != nil { @@ -237,7 +240,7 @@ func (c *Commands) generateIntentToken(intentID string) (string, error) { return base64.RawURLEncoding.EncodeToString(token), nil } -func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, attributes map[string][]string) (string, error) { +func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, session *ldap.Session) (string, error) { token, err := c.generateIntentToken(writeModel.AggregateID) if err != nil { return "", err @@ -246,6 +249,10 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte if err != nil { return "", err } + attributes := make(map[string][]string, len(session.Entry.Attributes)) + for _, item := range session.Entry.Attributes { + attributes[item.Name] = item.Values + } cmd := idpintent.NewLDAPSucceededEvent( ctx, IDPIntentAggregateFromWriteModel(&writeModel.WriteModel), @@ -254,6 +261,7 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte idpUser.GetPreferredUsername(), userID, attributes, + session.ExpiresAt(), ) err = c.pushAppendAndReduce(ctx, writeModel, cmd) if err != nil { @@ -273,7 +281,7 @@ func (c *Commands) FailIDPIntent(ctx context.Context, writeModel *IDPIntentWrite } func (c *Commands) GetIntentWriteModel(ctx context.Context, id, resourceOwner string) (*IDPIntentWriteModel, error) { - writeModel := NewIDPIntentWriteModel(id, resourceOwner) + writeModel := NewIDPIntentWriteModel(id, resourceOwner, c.maxIdPIntentLifetime) err := c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err diff --git a/internal/command/idp_intent_model.go b/internal/command/idp_intent_model.go index c6bc26ab06..07e0821813 100644 --- a/internal/command/idp_intent_model.go +++ b/internal/command/idp_intent_model.go @@ -2,6 +2,7 @@ package command import ( "net/url" + "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -29,18 +30,29 @@ type IDPIntentWriteModel struct { RequestID string Assertion *crypto.CryptoValue - State domain.IDPIntentState + State domain.IDPIntentState + succeededAt time.Time + maxIdPIntentLifetime time.Duration + expiresAt time.Time } -func NewIDPIntentWriteModel(id, resourceOwner string) *IDPIntentWriteModel { +func NewIDPIntentWriteModel(id, resourceOwner string, maxIdPIntentLifetime time.Duration) *IDPIntentWriteModel { return &IDPIntentWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: id, ResourceOwner: resourceOwner, }, + maxIdPIntentLifetime: maxIdPIntentLifetime, } } +func (wm *IDPIntentWriteModel) ExpiresAt() time.Time { + if wm.expiresAt.IsZero() { + return wm.succeededAt.Add(wm.maxIdPIntentLifetime) + } + return wm.expiresAt +} + func (wm *IDPIntentWriteModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { @@ -56,6 +68,8 @@ func (wm *IDPIntentWriteModel) Reduce() error { wm.reduceLDAPSucceededEvent(e) case *idpintent.FailedEvent: wm.reduceFailedEvent(e) + case *idpintent.ConsumedEvent: + wm.reduceConsumedEvent(e) } } return wm.WriteModel.Reduce() @@ -74,6 +88,7 @@ func (wm *IDPIntentWriteModel) Query() *eventstore.SearchQueryBuilder { idpintent.SAMLRequestEventType, idpintent.LDAPSucceededEventType, idpintent.FailedEventType, + idpintent.ConsumedEventType, ). Builder() } @@ -93,6 +108,8 @@ func (wm *IDPIntentWriteModel) reduceSAMLSucceededEvent(e *idpintent.SAMLSucceed wm.IDPUserName = e.IDPUserName wm.Assertion = e.Assertion wm.State = domain.IDPIntentStateSucceeded + wm.succeededAt = e.CreationDate() + wm.expiresAt = e.ExpiresAt } func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceededEvent) { @@ -102,6 +119,8 @@ func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceed wm.IDPUserName = e.IDPUserName wm.IDPEntryAttributes = e.EntryAttributes wm.State = domain.IDPIntentStateSucceeded + wm.succeededAt = e.CreationDate() + wm.expiresAt = e.ExpiresAt } func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededEvent) { @@ -112,6 +131,8 @@ func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededE wm.IDPAccessToken = e.IDPAccessToken wm.IDPIDToken = e.IDPIDToken wm.State = domain.IDPIntentStateSucceeded + wm.succeededAt = e.CreationDate() + wm.expiresAt = e.ExpiresAt } func (wm *IDPIntentWriteModel) reduceSAMLRequestEvent(e *idpintent.SAMLRequestEvent) { @@ -122,6 +143,10 @@ func (wm *IDPIntentWriteModel) reduceFailedEvent(e *idpintent.FailedEvent) { wm.State = domain.IDPIntentStateFailed } +func (wm *IDPIntentWriteModel) reduceConsumedEvent(e *idpintent.ConsumedEvent) { + wm.State = domain.IDPIntentStateConsumed +} + func IDPIntentAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { return &eventstore.Aggregate{ Type: idpintent.AggregateType, diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index 2400b9ee35..1be3971e87 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -4,8 +4,10 @@ import ( "context" "net/url" "testing" + "time" - "github.com/crewjam/saml" + crewjam_saml "github.com/crewjam/saml" + goldap "github.com/go-ldap/ldap/v3" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,6 +28,7 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/ldap" "github.com/zitadel/zitadel/internal/idp/providers/oauth" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" + "github.com/zitadel/zitadel/internal/idp/providers/saml" rep_idp "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/repository/instance" @@ -867,7 +870,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "ro", 0), }, res{ err: zerrors.ThrowInternal(nil, "id", "encryption failed"), @@ -888,7 +891,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "ro", 0), idpSession: &oauth.Session{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ @@ -922,6 +925,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { Crypted: []byte("accessToken"), }, "idToken", + time.Time{}, ) return event }(), @@ -930,7 +934,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), idpSession: &openid.Session{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ @@ -973,7 +977,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { ctx context.Context writeModel *IDPIntentWriteModel idpUser idp.User - assertion *saml.Assertion + session *saml.Session userID string } type res struct { @@ -998,7 +1002,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "ro", 0), }, res{ err: zerrors.ThrowInternal(nil, "id", "encryption failed"), @@ -1023,14 +1027,17 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { KeyID: "id", Crypted: []byte(""), }, + time.Time{}, ), ), ), }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), - assertion: &saml.Assertion{ID: "id"}, + writeModel: NewIDPIntentWriteModel("id", "instance", 0), + session: &saml.Session{ + Assertion: &crewjam_saml.Assertion{ID: "id"}, + }, idpUser: openid.NewUser(&oidc.UserInfo{ Subject: "id", UserInfoProfile: oidc.UserInfoProfile{ @@ -1061,14 +1068,17 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { KeyID: "id", Crypted: []byte(""), }, + time.Time{}, ), ), ), }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), - assertion: &saml.Assertion{ID: "id"}, + writeModel: NewIDPIntentWriteModel("id", "instance", 0), + session: &saml.Session{ + Assertion: &crewjam_saml.Assertion{ID: "id"}, + }, idpUser: openid.NewUser(&oidc.UserInfo{ Subject: "id", UserInfoProfile: oidc.UserInfoProfile{ @@ -1088,7 +1098,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.idpConfigEncryption, } - got, err := c.SucceedSAMLIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.assertion) + got, err := c.SucceedSAMLIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.session) require.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.token, got) }) @@ -1128,7 +1138,7 @@ func TestCommands_RequestSAMLIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), request: "request", }, res{}, @@ -1156,7 +1166,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { writeModel *IDPIntentWriteModel idpUser idp.User userID string - attributes map[string][]string + session *ldap.Session } type res struct { token string @@ -1180,7 +1190,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), }, res{ err: zerrors.ThrowInternal(nil, "id", "encryption failed"), @@ -1200,14 +1210,24 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { "username", "", map[string][]string{"id": {"id"}}, + time.Time{}, ), ), ), }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), - attributes: map[string][]string{"id": {"id"}}, + writeModel: NewIDPIntentWriteModel("id", "instance", 0), + session: &ldap.Session{ + Entry: &goldap.Entry{ + Attributes: []*goldap.EntryAttribute{ + { + Name: "id", + Values: []string{"id"}, + }, + }, + }, + }, idpUser: ldap.NewUser( "id", "", @@ -1235,7 +1255,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.idpConfigEncryption, } - got, err := c.SucceedLDAPIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.attributes) + got, err := c.SucceedLDAPIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.session) require.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.token, got) }) @@ -1275,7 +1295,7 @@ func TestCommands_FailIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), reason: "reason", }, res{ diff --git a/internal/command/session.go b/internal/command/session.go index d00e541e62..3c06c22967 100644 --- a/internal/command/session.go +++ b/internal/command/session.go @@ -17,6 +17,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/notification/senders" + "github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/repository/session" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -32,31 +33,33 @@ type SessionCommands struct { eventstore *eventstore.Eventstore eventCommands []eventstore.Command - hasher *crypto.Hasher - intentAlg crypto.EncryptionAlgorithm - totpAlg crypto.EncryptionAlgorithm - otpAlg crypto.EncryptionAlgorithm - createCode encryptedCodeWithDefaultFunc - createPhoneCode encryptedCodeGeneratorWithDefaultFunc - createToken func(sessionID string) (id string, token string, err error) - getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) - now func() time.Time + hasher *crypto.Hasher + intentAlg crypto.EncryptionAlgorithm + totpAlg crypto.EncryptionAlgorithm + otpAlg crypto.EncryptionAlgorithm + createCode encryptedCodeWithDefaultFunc + createPhoneCode encryptedCodeGeneratorWithDefaultFunc + createToken func(sessionID string) (id string, token string, err error) + getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) + now func() time.Time + maxIdPIntentLifetime time.Duration } func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands { return &SessionCommands{ - sessionCommands: cmds, - sessionWriteModel: session, - eventstore: c.eventstore, - hasher: c.userPasswordHasher, - intentAlg: c.idpConfigEncryption, - totpAlg: c.multifactors.OTP.CryptoMFA, - otpAlg: c.userEncryption, - createCode: c.newEncryptedCodeWithDefault, - createPhoneCode: c.newPhoneCode, - createToken: c.sessionTokenCreator, - getCodeVerifier: c.phoneCodeVerifierFromConfig, - now: time.Now, + sessionCommands: cmds, + sessionWriteModel: session, + eventstore: c.eventstore, + hasher: c.userPasswordHasher, + intentAlg: c.idpConfigEncryption, + totpAlg: c.multifactors.OTP.CryptoMFA, + otpAlg: c.userEncryption, + createCode: c.newEncryptedCodeWithDefault, + createPhoneCode: c.newPhoneCode, + createToken: c.sessionTokenCreator, + getCodeVerifier: c.phoneCodeVerifierFromConfig, + now: time.Now, + maxIdPIntentLifetime: c.maxIdPIntentLifetime, } } @@ -92,7 +95,7 @@ func CheckIntent(intentID, token string) SessionCommand { if err := crypto.CheckToken(cmd.intentAlg, token, intentID); err != nil { return nil, err } - cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "") + cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "", cmd.maxIdPIntentLifetime) err := cmd.eventstore.FilterToQueryReducer(ctx, cmd.intentWriteModel) if err != nil { return nil, err @@ -100,6 +103,9 @@ func CheckIntent(intentID, token string) SessionCommand { if cmd.intentWriteModel.State != domain.IDPIntentStateSucceeded { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded") } + if time.Now().After(cmd.intentWriteModel.ExpiresAt()) { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SAf42", "Errors.Intent.Expired") + } if cmd.intentWriteModel.UserID != "" { if cmd.intentWriteModel.UserID != cmd.sessionWriteModel.UserID { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser") @@ -168,6 +174,7 @@ func (s *SessionCommands) PasswordChecked(ctx context.Context, checkedAt time.Ti func (s *SessionCommands) IntentChecked(ctx context.Context, checkedAt time.Time) { s.eventCommands = append(s.eventCommands, session.NewIntentCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt)) + s.eventCommands = append(s.eventCommands, idpintent.NewConsumedEvent(ctx, IDPIntentAggregateFromWriteModel(&s.intentWriteModel.WriteModel))) } func (s *SessionCommands) WebAuthNChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement, rpid string) { diff --git a/internal/command/session_test.go b/internal/command/session_test.go index 60027d3a05..e65f32fb57 100644 --- a/internal/command/session_test.go +++ b/internal/command/session_test.go @@ -695,6 +695,7 @@ func TestCommands_updateSession(t *testing.T) { "userID2", nil, "", + time.Now().Add(time.Hour), ), ), ), @@ -757,6 +758,111 @@ func TestCommands_updateSession(t *testing.T) { err: zerrors.ThrowPermissionDenied(nil, "CRYPTO-CRYPTO", "Errors.Intent.InvalidToken"), }, }, + { + "set user, intent token already consumed", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, + "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false), + ), + eventFromEventPusher( + idpintent.NewSucceededEvent(context.Background(), + &idpintent.NewAggregate("intent", "instance1").Aggregate, + nil, + "idpUserID", + "idpUsername", + "userID", + nil, + "", + time.Now().Add(time.Hour), + ), + ), + eventFromEventPusher( + idpintent.NewConsumedEvent(context.Background(), + &idpintent.NewAggregate("intent", "instance1").Aggregate, + ), + ), + ), + ), + }, + args{ + ctx: authz.NewMockContext("instance1", "", ""), + checks: &SessionCommands{ + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), + sessionCommands: []SessionCommand{ + CheckUser("userID", "org1", &language.Afrikaans), + CheckIntent("intent", "aW50ZW50"), + }, + createToken: func(sessionID string) (string, string, error) { + return "tokenID", + "token", + nil + }, + intentAlg: decryption(nil), + now: func() time.Time { + return testNow + }, + }, + metadata: map[string][]byte{ + "key": []byte("value"), + }, + }, + res{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded"), + }, + }, + { + "set user, intent token already expired", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, + "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false), + ), + eventFromEventPusher( + idpintent.NewSucceededEvent(context.Background(), + &idpintent.NewAggregate("intent", "instance1").Aggregate, + nil, + "idpUserID", + "idpUsername", + "userID", + nil, + "", + time.Now().Add(-time.Hour), + ), + ), + ), + ), + }, + args{ + ctx: authz.NewMockContext("instance1", "", ""), + checks: &SessionCommands{ + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), + sessionCommands: []SessionCommand{ + CheckUser("userID", "org1", &language.Afrikaans), + CheckIntent("intent", "aW50ZW50"), + }, + createToken: func(sessionID string) (string, string, error) { + return "tokenID", + "token", + nil + }, + intentAlg: decryption(nil), + now: func() time.Time { + return testNow + }, + }, + metadata: map[string][]byte{ + "key": []byte("value"), + }, + }, + res{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SAf42", "Errors.Intent.Expired"), + }, + }, { "set user, intent, metadata and token", fields{ @@ -768,13 +874,14 @@ func TestCommands_updateSession(t *testing.T) { ), eventFromEventPusher( idpintent.NewSucceededEvent(context.Background(), - &idpintent.NewAggregate("id", "instance1").Aggregate, + &idpintent.NewAggregate("intent", "instance1").Aggregate, nil, "idpUserID", "idpUsername", "userID", nil, "", + time.Now().Add(time.Hour), ), ), ), @@ -783,6 +890,7 @@ func TestCommands_updateSession(t *testing.T) { "userID", "org1", testNow, &language.Afrikaans), session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, testNow), + idpintent.NewConsumedEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate), session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, map[string][]byte{"key": []byte("value")}), session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, @@ -842,13 +950,14 @@ func TestCommands_updateSession(t *testing.T) { ), eventFromEventPusher( idpintent.NewSucceededEvent(context.Background(), - &idpintent.NewAggregate("id", "instance1").Aggregate, + &idpintent.NewAggregate("intent", "instance1").Aggregate, nil, "idpUserID", "idpUsername", "", nil, "", + time.Now().Add(time.Hour), ), ), ), @@ -866,6 +975,7 @@ func TestCommands_updateSession(t *testing.T) { "userID", "org1", testNow, &language.Afrikaans), session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, testNow), + idpintent.NewConsumedEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate), session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID"), ), diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go index f6d39befe7..827dd61f73 100644 --- a/internal/config/systemdefaults/system_defaults.go +++ b/internal/config/systemdefaults/system_defaults.go @@ -7,15 +7,16 @@ import ( ) type SystemDefaults struct { - SecretGenerators SecretGenerators - PasswordHasher crypto.HashConfig - SecretHasher crypto.HashConfig - Multifactors MultifactorConfig - DomainVerification DomainVerification - Notifications Notifications - KeyConfig KeyConfig - DefaultQueryLimit uint64 - MaxQueryLimit uint64 + SecretGenerators SecretGenerators + PasswordHasher crypto.HashConfig + SecretHasher crypto.HashConfig + Multifactors MultifactorConfig + DomainVerification DomainVerification + Notifications Notifications + KeyConfig KeyConfig + DefaultQueryLimit uint64 + MaxQueryLimit uint64 + MaxIdPIntentLifetime time.Duration } type SecretGenerators struct { diff --git a/internal/domain/idp.go b/internal/domain/idp.go index e2571f6b0d..bea106298b 100644 --- a/internal/domain/idp.go +++ b/internal/domain/idp.go @@ -115,6 +115,7 @@ const ( IDPIntentStateStarted IDPIntentStateSucceeded IDPIntentStateFailed + IDPIntentStateConsumed idpIntentStateCount ) diff --git a/internal/idp/providers/apple/session.go b/internal/idp/providers/apple/session.go index eee68fa2a5..9395d84b2b 100644 --- a/internal/idp/providers/apple/session.go +++ b/internal/idp/providers/apple/session.go @@ -10,6 +10,8 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/oidc" ) +var _ idp.Session = (*Session)(nil) + // Session extends the [oidc.Session] with the formValues returned from the callback. // This enables to parse the user (name and email), which Apple only returns as form params on registration type Session struct { diff --git a/internal/idp/providers/azuread/session.go b/internal/idp/providers/azuread/session.go index 4b0a6fb844..169784fb58 100644 --- a/internal/idp/providers/azuread/session.go +++ b/internal/idp/providers/azuread/session.go @@ -3,6 +3,7 @@ package azuread import ( "context" "net/http" + "time" "github.com/zitadel/oidc/v3/pkg/client/rp" httphelper "github.com/zitadel/oidc/v3/pkg/http" @@ -12,6 +13,8 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/oauth" ) +var _ idp.Session = (*Session)(nil) + // Session extends the [oauth.Session] to be able to handle the id_token and to implement the [idp.SessionSupportsMigration] functionality type Session struct { *Provider @@ -79,6 +82,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return user, nil } +func (s *Session) ExpiresAt() time.Time { + if s.OAuthSession == nil { + return time.Time{} + } + return s.OAuthSession.ExpiresAt() +} + // Tokens returns the [oidc.Tokens] of the underlying [oauth.Session]. func (s *Session) Tokens() *oidc.Tokens[*oidc.IDTokenClaims] { return s.oauth().Tokens diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index 6df08a6998..5138812f3c 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -57,6 +57,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return &User{s.Tokens.IDTokenClaims}, nil } +func (s *Session) ExpiresAt() time.Time { + if s.Tokens == nil || s.Tokens.IDTokenClaims == nil { + return time.Time{} + } + return s.Tokens.IDTokenClaims.GetExpiration() +} + func (s *Session) validateToken(ctx context.Context, token string) (*oidc.IDTokenClaims, error) { logging.Debug("begin token validation") // TODO: be able to specify them in the template: https://github.com/zitadel/zitadel/issues/5322 diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go index 0a6a87ba3d..1679e35b61 100644 --- a/internal/idp/providers/ldap/session.go +++ b/internal/idp/providers/ldap/session.go @@ -96,6 +96,10 @@ func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) { ) } +func (s *Session) ExpiresAt() time.Time { + return time.Time{} // falls back to the default expiration time +} + func tryBind( server string, startTLS bool, diff --git a/internal/idp/providers/oauth/session.go b/internal/idp/providers/oauth/session.go index 247a7f8710..c9e175d1cf 100644 --- a/internal/idp/providers/oauth/session.go +++ b/internal/idp/providers/oauth/session.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + "time" "github.com/zitadel/oidc/v3/pkg/client/rp" httphelper "github.com/zitadel/oidc/v3/pkg/http" @@ -69,6 +70,13 @@ func (s *Session) FetchUser(ctx context.Context) (_ idp.User, err error) { return user, nil } +func (s *Session) ExpiresAt() time.Time { + if s.Tokens == nil { + return time.Time{} + } + return s.Tokens.Expiry +} + func (s *Session) authorize(ctx context.Context) (err error) { if s.Code == "" { return ErrCodeMissing diff --git a/internal/idp/providers/oidc/session.go b/internal/idp/providers/oidc/session.go index b17a3b0a0b..430a14e5bb 100644 --- a/internal/idp/providers/oidc/session.go +++ b/internal/idp/providers/oidc/session.go @@ -3,6 +3,7 @@ package oidc import ( "context" "errors" + "time" "github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -72,6 +73,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return u, nil } +func (s *Session) ExpiresAt() time.Time { + if s.Tokens == nil { + return time.Time{} + } + return s.Tokens.Expiry +} + func (s *Session) Authorize(ctx context.Context) (err error) { if s.Code == "" { return ErrCodeMissing diff --git a/internal/idp/providers/saml/session.go b/internal/idp/providers/saml/session.go index b0748d33a3..e2a1655a26 100644 --- a/internal/idp/providers/saml/session.go +++ b/internal/idp/providers/saml/session.go @@ -6,6 +6,7 @@ import ( "errors" "net/http" "net/url" + "time" "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" @@ -107,6 +108,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return userMapper, nil } +func (s *Session) ExpiresAt() time.Time { + if s.Assertion == nil || s.Assertion.Conditions == nil { + return time.Time{} + } + return s.Assertion.Conditions.NotOnOrAfter +} + func (s *Session) transientMappingID() (string, error) { for _, statement := range s.Assertion.AttributeStatements { for _, attribute := range statement.Attributes { diff --git a/internal/idp/session.go b/internal/idp/session.go index ab54bcabaa..fc593eb820 100644 --- a/internal/idp/session.go +++ b/internal/idp/session.go @@ -2,6 +2,7 @@ package idp import ( "context" + "time" ) // Session is the minimal implementation for a session of a 3rd party authentication [Provider] @@ -9,6 +10,7 @@ type Session interface { GetAuth(ctx context.Context) (content string, redirect bool) PersistentParameters() map[string]any FetchUser(ctx context.Context) (User, error) + ExpiresAt() time.Time } // SessionSupportsMigration is an optional extension to the Session interface. diff --git a/internal/integration/client.go b/internal/integration/client.go index e82a6bec55..f1bcfb41bd 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -672,6 +672,23 @@ func (i *Instance) CreatePasswordSession(t *testing.T, ctx context.Context, user createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime() } +func (i *Instance) CreateIntentSession(t *testing.T, ctx context.Context, userID, intentID, intentToken string) (id, token string, start, change time.Time) { + createResp, err := i.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{UserId: userID}, + }, + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: intentToken, + }, + }, + }) + require.NoError(t, err) + return createResp.GetSessionId(), createResp.GetSessionToken(), + createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime() +} + func (i *Instance) CreateProjectGrant(ctx context.Context, projectID, grantedOrgID string) *mgmt.AddProjectGrantResponse { resp, err := i.Client.Mgmt.AddProjectGrant(ctx, &mgmt.AddProjectGrantRequest{ GrantedOrgId: grantedOrgID, diff --git a/internal/integration/sink/server.go b/internal/integration/sink/server.go index 633ebf424f..8abb31a63e 100644 --- a/internal/integration/sink/server.go +++ b/internal/integration/sink/server.go @@ -17,6 +17,7 @@ import ( crewjam_saml "github.com/crewjam/saml" "github.com/go-chi/chi/v5" + goldap "github.com/go-ldap/ldap/v3" "github.com/gorilla/websocket" "github.com/sirupsen/logrus" "github.com/zitadel/logging" @@ -48,7 +49,7 @@ func CallURL(ch Channel) string { return u.String() } -func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { +func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { u := url.URL{ Scheme: "http", Host: host, @@ -59,6 +60,7 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, IDPID: idpID, IDPUserID: idpUserID, UserID: userID, + Expiry: expiry, }) if err != nil { return "", "", time.Time{}, uint64(0), err @@ -66,7 +68,7 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil } -func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { +func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { u := url.URL{ Scheme: "http", Host: host, @@ -77,6 +79,7 @@ func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string, IDPID: idpID, IDPUserID: idpUserID, UserID: userID, + Expiry: expiry, }) if err != nil { return "", "", time.Time{}, uint64(0), err @@ -84,7 +87,7 @@ func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string, return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil } -func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { +func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { u := url.URL{ Scheme: "http", Host: host, @@ -95,6 +98,7 @@ func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string, IDPID: idpID, IDPUserID: idpUserID, UserID: userID, + Expiry: expiry, }) if err != nil { return "", "", time.Time{}, uint64(0), err @@ -282,10 +286,11 @@ func readLoop(ws *websocket.Conn) (done chan error) { } type SuccessfulIntentRequest struct { - InstanceID string `json:"instance_id"` - IDPID string `json:"idp_id"` - IDPUserID string `json:"idp_user_id"` - UserID string `json:"user_id"` + InstanceID string `json:"instance_id"` + IDPID string `json:"idp_id"` + IDPUserID string `json:"idp_user_id"` + UserID string `json:"user_id"` + Expiry time.Time `json:"expiry"` } type SuccessfulIntentResponse struct { IntentID string `json:"intent_id"` @@ -376,6 +381,7 @@ func createSuccessfulOAuthIntent(ctx context.Context, cmd *command.Commands, req Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ AccessToken: "accessToken", + Expiry: req.Expiry, }, IDToken: "idToken", }, @@ -407,6 +413,7 @@ func createSuccessfulOIDCIntent(ctx context.Context, cmd *command.Commands, req Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ AccessToken: "accessToken", + Expiry: req.Expiry, }, IDToken: "idToken", }, @@ -431,9 +438,16 @@ func createSuccessfulSAMLIntent(ctx context.Context, cmd *command.Commands, req ID: req.IDPUserID, Attributes: map[string][]string{"attribute1": {"value1"}}, } - assertion := &crewjam_saml.Assertion{ID: "id"} + session := &saml.Session{ + Assertion: &crewjam_saml.Assertion{ + ID: "id", + Conditions: &crewjam_saml.Conditions{ + NotOnOrAfter: req.Expiry, + }, + }, + } - token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, assertion) + token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, session) if err != nil { return nil, err } @@ -465,8 +479,14 @@ func createSuccessfulLDAPIntent(ctx context.Context, cmd *command.Commands, req "", "", ) - attributes := map[string][]string{"id": {req.IDPUserID}, "username": {username}, "language": {lang.String()}} - token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, attributes) + session := &ldap.Session{Entry: &goldap.Entry{ + Attributes: []*goldap.EntryAttribute{ + {Name: "id", Values: []string{req.IDPUserID}}, + {Name: "username", Values: []string{username}}, + {Name: "language", Values: []string{lang.String()}}, + }, + }} + token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, session) if err != nil { return nil, err } diff --git a/internal/repository/idpintent/eventstore.go b/internal/repository/idpintent/eventstore.go index ea94803973..6bec32c735 100644 --- a/internal/repository/idpintent/eventstore.go +++ b/internal/repository/idpintent/eventstore.go @@ -11,4 +11,5 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, SAMLRequestEventType, SAMLRequestEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, LDAPSucceededEventType, LDAPSucceededEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, FailedEventType, FailedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, ConsumedEventType, eventstore.GenericEventMapper[ConsumedEvent]) } diff --git a/internal/repository/idpintent/intent.go b/internal/repository/idpintent/intent.go index 27e6391f95..e4ee28cae9 100644 --- a/internal/repository/idpintent/intent.go +++ b/internal/repository/idpintent/intent.go @@ -3,6 +3,7 @@ package idpintent import ( "context" "net/url" + "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" @@ -16,6 +17,7 @@ const ( SAMLRequestEventType = instanceEventTypePrefix + "saml.requested" LDAPSucceededEventType = instanceEventTypePrefix + "ldap.succeeded" FailedEventType = instanceEventTypePrefix + "failed" + ConsumedEventType = instanceEventTypePrefix + "consumed" ) type StartedEvent struct { @@ -79,6 +81,7 @@ type SucceededEvent struct { IDPAccessToken *crypto.CryptoValue `json:"idpAccessToken,omitempty"` IDPIDToken string `json:"idpIdToken,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` } func NewSucceededEvent( @@ -90,6 +93,7 @@ func NewSucceededEvent( userID string, idpAccessToken *crypto.CryptoValue, idpIDToken string, + expiresAt time.Time, ) *SucceededEvent { return &SucceededEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -103,6 +107,7 @@ func NewSucceededEvent( UserID: userID, IDPAccessToken: idpAccessToken, IDPIDToken: idpIDToken, + ExpiresAt: expiresAt, } } @@ -136,6 +141,7 @@ type SAMLSucceededEvent struct { UserID string `json:"userId,omitempty"` Assertion *crypto.CryptoValue `json:"assertion,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` } func NewSAMLSucceededEvent( @@ -146,6 +152,7 @@ func NewSAMLSucceededEvent( idpUserName, userID string, assertion *crypto.CryptoValue, + expiresAt time.Time, ) *SAMLSucceededEvent { return &SAMLSucceededEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -158,6 +165,7 @@ func NewSAMLSucceededEvent( IDPUserName: idpUserName, UserID: userID, Assertion: assertion, + ExpiresAt: expiresAt, } } @@ -233,6 +241,7 @@ type LDAPSucceededEvent struct { UserID string `json:"userId,omitempty"` EntryAttributes map[string][]string `json:"user,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` } func NewLDAPSucceededEvent( @@ -243,6 +252,7 @@ func NewLDAPSucceededEvent( idpUserName, userID string, attributes map[string][]string, + expiresAt time.Time, ) *LDAPSucceededEvent { return &LDAPSucceededEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -255,6 +265,7 @@ func NewLDAPSucceededEvent( IDPUserName: idpUserName, UserID: userID, EntryAttributes: attributes, + ExpiresAt: expiresAt, } } @@ -320,3 +331,32 @@ func FailedEventMapper(event eventstore.Event) (eventstore.Event, error) { return e, nil } + +type ConsumedEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func NewConsumedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *ConsumedEvent { + return &ConsumedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + ConsumedEventType, + ), + } +} + +func (e *ConsumedEvent) Payload() interface{} { + return e +} + +func (e *ConsumedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *ConsumedEvent) SetBaseEvent(base *eventstore.BaseEvent) { + e.BaseEvent = *base +} diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index d7dc18898b..8254b82b45 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -554,6 +554,7 @@ Errors: StateMissing: В заявката липсва параметър състояние NotStarted: Намерението не е стартирано или вече е прекратено NotSucceeded: Намерението не е успешно + Expired: Намерението е изтекло TokenCreationFailed: Неуспешно създаване на токен InvalidToken: Знакът за намерение е невалиден OtherUser: Намерение, предназначено за друг потребител diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 80db4952f9..bb4172fbff 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -534,6 +534,7 @@ Errors: StateMissing: V požadavku chybí parametr stavu NotStarted: Záměr nebyl zahájen nebo již byl ukončen NotSucceeded: Záměr nebyl úspěšný + Expired: Záměr vypršel TokenCreationFailed: Vytvoření tokenu selhalo InvalidToken: Token záměru je neplatný OtherUser: Záměr určený pro jiného uživatele diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index dcb3ac5c71..a24ce7c933 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: State parameter fehlt im Request NotStarted: Intent wurde nicht gestartet oder wurde bereits beendet NotSucceeded: Intent war nicht erfolgreich + Expired: Intent ist abgelaufen TokenCreationFailed: Tokenerstellung schlug fehl InvalidToken: Intent Token ist ungültig OtherUser: Intent ist für anderen Benutzer gedacht diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index bd8d26d727..e8f2781de1 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: State parameter is missing in the request NotStarted: Intent is not started or was already terminated NotSucceeded: Intent has not succeeded + Expired: Intent has expired TokenCreationFailed: Token creation failed InvalidToken: Intent Token is invalid OtherUser: Intent meant for another user diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 9f11b63964..b91d055f70 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: Falta un parámetro de estado en la solicitud NotStarted: La intención no se ha iniciado o ya ha finalizado NotSucceeded: Intento fallido + Expired: La intención ha expirado TokenCreationFailed: Fallo en la creación del token InvalidToken: El token de la intención no es válido OtherUser: Destinado a otro usuario diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index ff8393befc..98f2bee9a0 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: Paramètre d'état manquant dans la requête NotStarted: Intent n'a pas démarré ou s'est déjà terminé NotSucceeded: l'intention n'a pas abouti + Expired: L'intention a expiré TokenCreationFailed: La création du token a échoué InvalidToken: Le jeton d'intention n'est pas valide OtherUser: Intention destinée à un autre utilisateur diff --git a/internal/static/i18n/hu.yaml b/internal/static/i18n/hu.yaml index b17c6a1225..5becd6e606 100644 --- a/internal/static/i18n/hu.yaml +++ b/internal/static/i18n/hu.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: A kérésből hiányzik a State paraméter NotStarted: Az intent nem indult el, vagy már befejeződött NotSucceeded: Az intent nem sikerült + Expired: A kérésből lejárt TokenCreationFailed: A token létrehozása nem sikerült InvalidToken: Az Intent Token érvénytelen OtherUser: Az intent egy másik felhasználónak szól diff --git a/internal/static/i18n/id.yaml b/internal/static/i18n/id.yaml index 56a454e71d..0108d7618b 100644 --- a/internal/static/i18n/id.yaml +++ b/internal/static/i18n/id.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: Parameter status tidak ada dalam permintaan NotStarted: Niat belum dimulai atau sudah dihentikan NotSucceeded: Niatnya belum berhasil + Expired: Kode sudah habis masa berlakunya TokenCreationFailed: Pembuatan token gagal InvalidToken: Token Niat tidak valid OtherUser: Maksudnya ditujukan untuk pengguna lain diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 6713abf2e1..750c48471a 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: parametro di stato mancante nella richiesta NotStarted: l'intento non è stato avviato o è già stato terminato NotSucceeded: l'intento non è andato a buon fine + Expired: L'intento è scaduto TokenCreationFailed: creazione del token fallita InvalidToken: Il token dell'intento non è valido OtherUser: Intento destinato a un altro utente diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index f57d0f6661..fcd7920999 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: リクエストに State パラメータがありません NotStarted: インテントが開始されなかったか、既に終了している NotSucceeded: インテントが成功しなかった + Expired: 意図の有効期限が切れました TokenCreationFailed: トークンの作成に失敗しました InvalidToken: インテントのトークンが無効である OtherUser: 他のユーザーを意図している diff --git a/internal/static/i18n/ko.yaml b/internal/static/i18n/ko.yaml index d238142e01..d83af62235 100644 --- a/internal/static/i18n/ko.yaml +++ b/internal/static/i18n/ko.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: 요청에 상태 매개변수가 누락되었습니다 NotStarted: 의도가 시작되지 않았거나 이미 종료되었습니다 NotSucceeded: 의도가 성공하지 않았습니다 + Expired: 의도의 유효 기간이 만료되었습니다 TokenCreationFailed: 토큰 생성 실패 InvalidToken: 의도 토큰이 유효하지 않습니다 OtherUser: 다른 사용자를 위한 의도입니다 diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 898ed67360..7126925279 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -535,6 +535,7 @@ Errors: StateMissing: Параметарот State недостасува во барањето NotStarted: Намерата не е започната или веќе завршена NotSucceeded: Намерата не е успешна + Expired: Намерата е истечена TokenCreationFailed: Неуспешно креирање на токен InvalidToken: Токенот за намера е невалиден OtherUser: Намерата е за друг корисник diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 882c58a4f2..a398e4b770 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: Staat parameter ontbreekt in het verzoek NotStarted: Intentie is niet gestart of was al beëindigd NotSucceeded: Intentie is niet geslaagd + Expired: Intentie is verlopen TokenCreationFailed: Token aanmaken mislukt InvalidToken: Intentie Token is ongeldig OtherUser: Intentie bedoeld voor een andere gebruiker diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 13125bc2a9..049a189930 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: Brak parametru stanu w żądaniu NotStarted: Intencja nie została rozpoczęta lub już się zakończyła NotSucceeded: intencja nie powiodła się + Expired: Intencja wygasła TokenCreationFailed: Tworzenie tokena nie powiodło się InvalidToken: Token intencji jest nieprawidłowy OtherUser: Intencja przeznaczona dla innego użytkownika diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 4ab3573c2b..09a5fc02c5 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -535,6 +535,7 @@ Errors: StateMissing: O parâmetro de estado está faltando na solicitação NotStarted: A intenção não foi iniciada ou já foi encerrada NotSucceeded: A intenção não teve sucesso + Expired: A intenção expirou TokenCreationFailed: Falha na criação do token InvalidToken: O token da intenção é inválido OtherUser: Intenção destinada a outro usuário diff --git a/internal/static/i18n/ro.yaml b/internal/static/i18n/ro.yaml index 48790da9e5..9010e57032 100644 --- a/internal/static/i18n/ro.yaml +++ b/internal/static/i18n/ro.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: Parametrul de stare lipsește în cerere NotStarted: Intenția nu este pornită sau a fost deja terminată NotSucceeded: Intenția nu a reușit + Expired: Intenția a expirat TokenCreationFailed: Crearea token-ului a eșuat InvalidToken: Token-ul intenției este invalid OtherUser: Intenția este destinată altui utilizator diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 64a8ef8013..38b2847637 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -525,6 +525,7 @@ Errors: StateMissing: В запросе отсутствует параметр State NotStarted: Намерение не начато или уже прекращено NotSucceeded: Намерение не увенчалось успехом + Epired: Намерение истекло TokenCreationFailed: Не удалось создать токен InvalidToken: Маркер намерения недействителен OtherUser: Намерение, предназначенное для другого пользователя diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index 2c292976d3..ed4b863886 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: State-parameter saknas i begäran NotStarted: Avsikten har inte startat eller har redan avslutats NotSucceeded: Avsikten har inte lyckats + Expired: Avsikten har gått ut TokenCreationFailed: Token-skapande misslyckades InvalidToken: Avsiktstoken är ogiltig OtherUser: Avsikten är avsedd för en annan användare diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index d4b36df7ff..03aa168a50 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: 请求中缺少状态参数 NotStarted: 意图没有开始或已经结束 NotSucceeded: 意图不成功 + Expired: 意图已过期 TokenCreationFailed: 令牌创建失败 InvalidToken: 意图令牌是无效的 OtherUser: 意图是为另一个用户准备的 From a626678004ee6a6ee02d814af2daebf673deed15 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Tue, 6 May 2025 08:15:45 +0200 Subject: [PATCH 25/26] fix(setup): execute s54 (#9849) # Which Problems Are Solved Step 54 was not executed during setup. # How the Problems Are Solved Added the step to setup jobs # Additional Changes none # Additional Context - the step was added in https://github.com/zitadel/zitadel/pull/9837 - thanks to @zhirschtritt for raising this. --- cmd/setup/config.go | 1 + cmd/setup/setup.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 4742b94c7b..127f9d7599 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -150,6 +150,7 @@ type Steps struct { s51IDPTemplate6RootCA *IDPTemplate6RootCA s52IDPTemplate6LDAP2 *IDPTemplate6LDAP2 s53InitPermittedOrgsFunction *InitPermittedOrgsFunction53 + s54InstancePositionIndex *InstancePositionIndex } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index f4df9fc71b..eead1980ed 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -212,6 +212,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s51IDPTemplate6RootCA = &IDPTemplate6RootCA{dbClient: dbClient} steps.s52IDPTemplate6LDAP2 = &IDPTemplate6LDAP2{dbClient: dbClient} steps.s53InitPermittedOrgsFunction = &InitPermittedOrgsFunction53{dbClient: dbClient} + steps.s54InstancePositionIndex = &InstancePositionIndex{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -254,6 +255,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s51IDPTemplate6RootCA, steps.s52IDPTemplate6LDAP2, steps.s53InitPermittedOrgsFunction, + steps.s54InstancePositionIndex, } { setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") if setupErr != nil { From 8cb1d24b36d4c7a588210c1a1649fa3b2a77dc7d Mon Sep 17 00:00:00 2001 From: Zach Hirschtritt Date: Tue, 6 May 2025 02:38:19 -0400 Subject: [PATCH 26/26] fix: add user id index on sessions8 (#9834) # Which Problems Are Solved When a user changes their password, Zitadel needs to terminate all of that user's active sessions. This query can take many seconds on deployments with large session and user tables. This happens as part of session projection handling, so doesn't directly impact user experience, but potentially bogs down the projection handler which isn't great. In the future, this index could be used to power a "see all of my current sessions" feature in Zitadel. # How the Problems Are Solved Adds new index on `user_id` column on `projections.sessions8` table. Alternatively, we can index on `(instance_id, user_id)` instead but opted for keeping the index smaller as we already index on `instance_id` separately. # Additional Changes None # Additional Context None --------- Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com> --- internal/query/projection/session.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/query/projection/session.go b/internal/query/projection/session.go index 196a352190..53eba54efb 100644 --- a/internal/query/projection/session.go +++ b/internal/query/projection/session.go @@ -87,6 +87,7 @@ func (*sessionProjection) Init() *old_handler.Check { SessionColumnUserAgentFingerprintID+"_idx", []string{SessionColumnUserAgentFingerprintID}, )), + handler.WithIndex(handler.NewIndex(SessionColumnUserID+"_idx", []string{SessionColumnUserID})), ), ) }
Reorder @@ -48,7 +48,7 @@ actions matTooltip="{{ 'ACTIONS.REMOVE' | translate }}" color="warn" - (click)="removeTarget(i)" + (click)="removeTarget(i, form)" mat-icon-button > @@ -65,7 +65,7 @@ {{ 'ACTIONS.BACK' | translate }} - diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts index e04368f8f4..60b4025650 100644 --- a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts @@ -14,7 +14,7 @@ import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { MatButtonModule } from '@angular/material/button'; import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { ReplaySubject, switchMap } from 'rxjs'; +import { ObservedValueOf, ReplaySubject, shareReplay, switchMap } from 'rxjs'; import { MatRadioModule } from '@angular/material/radio'; import { ActionService } from 'src/app/services/action.service'; import { ToastService } from 'src/app/services/toast.service'; @@ -23,14 +23,13 @@ import { InputModule } from 'src/app/modules/input/input.module'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MessageInitShape } from '@bufbuild/protobuf'; import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb'; -import { ExecutionTargetTypeSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb'; import { MatSelectModule } from '@angular/material/select'; import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; -import { startWith } from 'rxjs/operators'; +import { map, startWith } from 'rxjs/operators'; import { TypeSafeCellDefModule } from 'src/app/directives/type-safe-cell-def/type-safe-cell-def.module'; import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop'; -import { toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { toSignal } from '@angular/core/rxjs-interop'; import { minArrayLengthValidator } from '../../../form-field/validators/validators'; import { ProjectRoleChipModule } from '../../../project-role-chip/project-role-chip.module'; import { MatTooltipModule } from '@angular/material/tooltip'; @@ -72,11 +71,12 @@ export class ActionsTwoAddActionTargetComponent { } @Output() public readonly back = new EventEmitter(); - @Output() public readonly continue = new EventEmitter[]>(); + @Output() public readonly continue = new EventEmitter(); private readonly preselectedTargetIds$ = new ReplaySubject(1); - protected readonly form: ReturnType; + protected readonly form$: ReturnType; + protected readonly targets: ReturnType; private readonly selectedTargetIds: Signal; protected readonly selectableTargets: Signal; @@ -87,26 +87,27 @@ export class ActionsTwoAddActionTargetComponent { private readonly actionService: ActionService, private readonly toast: ToastService, ) { - this.form = this.buildForm(); + this.form$ = this.buildForm().pipe(shareReplay({ refCount: true, bufferSize: 1 })); this.targets = this.listTargets(); - this.selectedTargetIds = this.getSelectedTargetIds(this.form); - this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds); + this.selectedTargetIds = this.getSelectedTargetIds(this.form$); + this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds, this.form$); this.dataSource = this.getDataSource(this.targets, this.selectedTargetIds); } private buildForm() { - const preselectedTargetIds = toSignal(this.preselectedTargetIds$, { initialValue: [] as string[] }); - - return computed(() => { - return this.fb.group({ - autocomplete: new FormControl('', { nonNullable: true }), - selectedTargetIds: new FormControl(preselectedTargetIds(), { - nonNullable: true, - validators: [minArrayLengthValidator(1)], - }), - }); - }); + return this.preselectedTargetIds$.pipe( + startWith([] as string[]), + map((preselectedTargetIds) => { + return this.fb.group({ + autocomplete: new FormControl('', { nonNullable: true }), + selectedTargetIds: new FormControl(preselectedTargetIds, { + nonNullable: true, + validators: [minArrayLengthValidator(1)], + }), + }); + }), + ); } private listTargets() { @@ -129,25 +130,35 @@ export class ActionsTwoAddActionTargetComponent { return computed(targetsSignal); } - private getSelectedTargetIds(form: typeof this.form) { - const selectedTargetIds$ = toObservable(form).pipe( - startWith(form()), - switchMap((form) => { - const { selectedTargetIds } = form.controls; + private getSelectedTargetIds(form$: typeof this.form$) { + const selectedTargetIds$ = form$.pipe( + switchMap(({ controls: { selectedTargetIds } }) => { return selectedTargetIds.valueChanges.pipe(startWith(selectedTargetIds.value)); }), ); return toSignal(selectedTargetIds$, { requireSync: true }); } - private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal) { - return computed(() => { + private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal, form$: typeof this.form$) { + const autocomplete$ = form$.pipe( + switchMap(({ controls: { autocomplete } }) => { + return autocomplete.valueChanges.pipe(startWith(autocomplete.value)); + }), + ); + const autocompleteSignal = toSignal(autocomplete$, { requireSync: true }); + + const unselectedTargets = computed(() => { const targetsCopy = new Map(targets().targets); for (const selectedTargetId of selectedTargetIds()) { targetsCopy.delete(selectedTargetId); } return Array.from(targetsCopy.values()); }); + + return computed(() => { + const autocomplete = autocompleteSignal().toLowerCase(); + return unselectedTargets().filter(({ name }) => name.toLowerCase().includes(autocomplete)); + }); } private getDataSource(targetsSignal: typeof this.targets, selectedTargetIdsSignal: Signal) { @@ -178,46 +189,39 @@ export class ActionsTwoAddActionTargetComponent { return dataSource; } - protected addTarget(target: Target) { - const { selectedTargetIds } = this.form().controls; + protected addTarget(target: Target, form: ObservedValueOf) { + const { selectedTargetIds } = form.controls; selectedTargetIds.setValue([target.id, ...selectedTargetIds.value]); - this.form().controls.autocomplete.setValue(''); + form.controls.autocomplete.setValue(''); } - protected removeTarget(index: number) { - const { selectedTargetIds } = this.form().controls; + protected removeTarget(index: number, form: ObservedValueOf) { + const { selectedTargetIds } = form.controls; const data = [...selectedTargetIds.value]; data.splice(index, 1); selectedTargetIds.setValue(data); } - protected drop(event: CdkDragDrop) { - const { selectedTargetIds } = this.form().controls; + protected drop(event: CdkDragDrop, form: ObservedValueOf) { + const { selectedTargetIds } = form.controls; const data = [...selectedTargetIds.value]; moveItemInArray(data, event.previousIndex, event.currentIndex); selectedTargetIds.setValue(data); } - protected handleEnter(event: Event) { + protected handleEnter(event: Event, form: ObservedValueOf) { const selectableTargets = this.selectableTargets(); if (selectableTargets.length !== 1) { return; } event.preventDefault(); - this.addTarget(selectableTargets[0]); + this.addTarget(selectableTargets[0], form); } protected submit() { - const selectedTargets = this.selectedTargetIds().map((value) => ({ - type: { - case: 'target' as const, - value, - }, - })); - - this.continue.emit(selectedTargets); + this.continue.emit(this.selectedTargetIds()); } protected trackTarget(_: number, target: Target) { diff --git a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.html b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.html index 37d4f89dd0..717ee8f850 100644 --- a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.html +++ b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.html @@ -26,6 +26,9 @@ {{ 'ACTIONSTWO.TARGET.CREATE.TYPES.' + type | translate }} + + {{ 'ACTIONSTWO.TARGET.CREATE.TYPES_DESCRIPTION' | translate }} + diff --git a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss index 34a7d5203d..a68e1e87cb 100644 --- a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss +++ b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss @@ -23,3 +23,7 @@ .name-hint { font-size: 12px; } + +.types-description { + white-space: pre-line; +} diff --git a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html index a6bde66e41..23b3b4bb89 100644 --- a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html +++ b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html @@ -1,4 +1,7 @@

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

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

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

setTimeout(res, 1000)); @@ -86,4 +88,6 @@ export class ActionsTwoTargetsComponent { this.toast.showError(error); } } + + protected readonly InfoSectionType = InfoSectionType; } diff --git a/console/src/app/modules/actions-two/actions-two.module.ts b/console/src/app/modules/actions-two/actions-two.module.ts index 45d70193f9..a940264eb2 100644 --- a/console/src/app/modules/actions-two/actions-two.module.ts +++ b/console/src/app/modules/actions-two/actions-two.module.ts @@ -20,6 +20,7 @@ import { ProjectRoleChipModule } from '../project-role-chip/project-role-chip.mo import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module'; import { MatSelectModule } from '@angular/material/select'; import { MatIconModule } from '@angular/material/icon'; +import { InfoSectionModule } from '../info-section/info-section.module'; @NgModule({ declarations: [ @@ -47,6 +48,7 @@ import { MatIconModule } from '@angular/material/icon'; TypeSafeCellDefModule, ProjectRoleChipModule, ActionConditionPipeModule, + InfoSectionModule, ], exports: [ActionsTwoActionsComponent, ActionsTwoTargetsComponent, ActionsTwoTargetsTableComponent], }) diff --git a/console/src/app/modules/settings-list/settings.ts b/console/src/app/modules/settings-list/settings.ts index 79b92e2214..c96431fa30 100644 --- a/console/src/app/modules/settings-list/settings.ts +++ b/console/src/app/modules/settings-list/settings.ts @@ -231,6 +231,7 @@ export const ACTIONS: SidenavSetting = { // todo: figure out roles [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], }, + beta: true, }; export const ACTIONS_TARGETS: SidenavSetting = { @@ -241,4 +242,5 @@ export const ACTIONS_TARGETS: SidenavSetting = { // todo: figure out roles [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], }, + beta: true, }; diff --git a/console/src/app/modules/sidenav/sidenav.component.html b/console/src/app/modules/sidenav/sidenav.component.html index 277686852b..78e21e79f6 100644 --- a/console/src/app/modules/sidenav/sidenav.component.html +++ b/console/src/app/modules/sidenav/sidenav.component.html @@ -28,6 +28,7 @@ [attr.data-e2e]="'sidenav-element-' + setting.id" > {{ setting.i18nKey | translate }} + {{ 'SETTINGS.BETA' | translate }} diff --git a/console/src/app/modules/sidenav/sidenav.component.scss b/console/src/app/modules/sidenav/sidenav.component.scss index 383857751c..bb55a6999d 100644 --- a/console/src/app/modules/sidenav/sidenav.component.scss +++ b/console/src/app/modules/sidenav/sidenav.component.scss @@ -90,6 +90,10 @@ flex-shrink: 0; } + .state { + margin-left: 0.5rem; + } + &:hover { span { opacity: 1; diff --git a/console/src/app/modules/sidenav/sidenav.component.ts b/console/src/app/modules/sidenav/sidenav.component.ts index 33539750a2..4b73491f08 100644 --- a/console/src/app/modules/sidenav/sidenav.component.ts +++ b/console/src/app/modules/sidenav/sidenav.component.ts @@ -11,6 +11,7 @@ export interface SidenavSetting { [PolicyComponentServiceType.ADMIN]?: string[]; }; showWarn?: boolean; + beta?: boolean; } @Component({ diff --git a/console/src/app/pages/actions/actions.component.html b/console/src/app/pages/actions/actions.component.html index 4c55308ced..af936e2351 100644 --- a/console/src/app/pages/actions/actions.component.html +++ b/console/src/app/pages/actions/actions.component.html @@ -6,6 +6,9 @@ info_outline + + {{ 'DESCRIPTIONS.ACTIONS.ACTIONSTWO_NOTE' | translate }} +

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

= new Subject(); + protected maxActions: number | null = null; + protected ActionState = ActionState; constructor( private mgmtService: ManagementService, breadcrumbService: BreadcrumbService, private dialog: MatDialog, private toast: ToastService, + destroyRef: DestroyRef, ) { const bread: Breadcrumb = { type: BreadcrumbType.ORG, @@ -45,31 +45,24 @@ export class ActionsComponent implements OnDestroy { }; breadcrumbService.setBreadcrumb([bread]); - this.getFlowTypes(); + this.getFlowTypes().then(); - this.typeControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { + this.typeControl.valueChanges.pipe(takeUntilDestroyed(destroyRef)).subscribe((value) => { this.loadFlow((value as FlowType.AsObject).id); }); } - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - private getFlowTypes(): Promise { - return this.mgmtService - .listFlowTypes() - .then((resp) => { - this.typesForSelection = resp.resultList; - if (!this.flow && resp.resultList[0]) { - const type = resp.resultList[0]; - this.typeControl.setValue(type); - } - }) - .catch((error: any) => { - this.toast.showError(error); - }); + private async getFlowTypes(): Promise { + try { + let resp = await this.mgmtService.listFlowTypes(); + this.typesForSelection = resp.resultList; + if (!this.flow && resp.resultList[0]) { + const type = resp.resultList[0]; + this.typeControl.setValue(type); + } + } catch (error) { + this.toast.showError(error); + } } private loadFlow(id: string) { @@ -106,7 +99,7 @@ export class ActionsComponent implements OnDestroy { }); } - public openAddTrigger(flow: FlowType.AsObject, trigger?: TriggerType.AsObject): void { + protected openAddTrigger(flow: FlowType.AsObject, trigger?: TriggerType.AsObject): void { const dialogRef = this.dialog.open(AddFlowDialogComponent, { data: { flowType: flow, @@ -119,7 +112,7 @@ export class ActionsComponent implements OnDestroy { if (req) { this.mgmtService .setTriggerActions(req.getActionIdsList(), req.getFlowType(), req.getTriggerType()) - .then((resp) => { + .then(() => { this.toast.showInfo('FLOWS.FLOWCHANGED', true); this.loadFlow(flow.id); }) @@ -157,7 +150,7 @@ export class ActionsComponent implements OnDestroy { } } - public removeTriggerActionsList(index: number) { + protected removeTriggerActionsList(index: number) { if (this.flow.type && this.flow.triggerActionsList && this.flow.triggerActionsList[index]) { const dialogRef = this.dialog.open(WarnDialogComponent, { data: { diff --git a/console/src/app/pages/instance/instance.component.ts b/console/src/app/pages/instance/instance.component.ts index 546553132d..e52cdd7198 100644 --- a/console/src/app/pages/instance/instance.component.ts +++ b/console/src/app/pages/instance/instance.component.ts @@ -42,8 +42,6 @@ import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { EnvironmentService } from 'src/app/services/environment.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { NewFeatureService } from '../../services/new-feature.service'; -import { withLatestFromSynchronousFix } from '../../utils/withLatestFromSynchronousFix'; @Component({ selector: 'cnsl-instance', templateUrl: './instance.component.html', @@ -106,7 +104,6 @@ export class InstanceComponent { private readonly envService: EnvironmentService, activatedRoute: ActivatedRoute, private readonly destroyRef: DestroyRef, - private readonly featureService: NewFeatureService, ) { this.loadMembers(); @@ -139,32 +136,7 @@ export class InstanceComponent { } private getSettingsList(): Observable { - const features$ = this.getFeatures().pipe(shareReplay({ refCount: true, bufferSize: 1 })); - - const actionsEnabled$ = features$.pipe(map((features) => features?.actions?.enabled)); - - return this.authService - .isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin || []) - .pipe( - withLatestFromSynchronousFix(actionsEnabled$), - map(([settings, actionsEnabled]) => - settings - .filter((setting) => actionsEnabled || setting.id !== ACTIONS.id) - .filter((setting) => actionsEnabled || setting.id !== ACTIONS_TARGETS.id), - ), - ); - } - - private getFeatures() { - return defer(() => this.featureService.getInstanceFeatures()).pipe( - timeout(1000), - catchError((error) => { - if (!(error instanceof TimeoutError)) { - this.toast.showError(error); - } - return of(undefined); - }), - ); + return this.authService.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin || []); } public loadMembers(): void { diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 4bd94d5ce6..30d9c53763 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Потоци", "DESCRIPTION": "Изберете поток за удостоверяване и активирайте вашето действие при конкретно събитие в този поток." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, нова и подобрена версия на Actions, вече е налична. Настоящата версия все още е достъпна, но бъдещото развитие ще бъде фокусирано върху новата, която в крайна сметка ще замени текущата версия." }, "SETTINGS": { "INSTANCE": { @@ -528,6 +529,7 @@ "APPLY": "Прилагам" }, "ACTIONSTWO": { + "BETA_NOTE": "В момента използвате новата версия Actions V2, която е в бета фаза. Предишната версия 1 все още е достъпна, но ще бъде спряна в бъдеще. Моля, съобщавайте за всякакви проблеми или изпратете обратна връзка.", "EXECUTION": { "TITLE": "Действия", "DESCRIPTION": "Действията ви позволяват да изпълнявате персонализиран код в отговор на API заявки, събития или специфични функции. Използвайте ги, за да разширите Zitadel, да автоматизирате работни процеси и да се интегрирате с други системи.", @@ -618,6 +620,7 @@ "restCall": "REST извикване", "restAsync": "REST асинхронно" }, + "TYPES_DESCRIPTION": "Webhook, обаждането обработва кода на състоянието, но отговорът е без значение\nCall, обаждането обработва кода на състоянието и отговора\nAsync, обаждането не обработва нито кода на състоянието, нито отговора, но може да бъде извикано паралелно с други цели", "ENDPOINT": "Крайна точка", "ENDPOINT_DESCRIPTION": "Въведете крайната точка, където се хоства вашият код. Уверете се, че е достъпна за нас!", "TIMEOUT": "Време за изчакване", @@ -1507,7 +1510,8 @@ "APPEARANCE": "Външен вид", "OTHER": "други", "STORAGE": "Съхранение" - } + }, + "BETA": "БЕТА" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index 6dd066789e..ee43e86822 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flows", "DESCRIPTION": "Vyberte proces autentizace a spusťte vaši akci na konkrétní události v rámci tohoto procesu." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, nová a vylepšená verze Actions, je nyní k dispozici. Aktuální verze je stále přístupná, ale budoucí vývoj se zaměří na novou verzi, která nakonec nahradí tu současnou." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "Platit" }, "ACTIONSTWO": { + "BETA_NOTE": "Aktuálně používáte novou verzi Actions V2, která je v beta verzi. Předchozí verze 1 je stále k dispozici, ale v budoucnu bude ukončena. Prosím, hlaste jakékoliv problémy nebo zpětnou vazbu.", "EXECUTION": { "TITLE": "Akce", "DESCRIPTION": "Akce vám umožňují spouštět vlastní kód v reakci na požadavky API, události nebo specifické funkce. Použijte je k rozšíření Zitadel, automatizaci pracovních postupů a integraci s dalšími systémy.", @@ -619,6 +621,7 @@ "restCall": "REST Volání", "restAsync": "REST Asynchronní" }, + "TYPES_DESCRIPTION": "Webhook, volání zpracovává stavový kód, ale odpověď je irelevantní\nCall, volání zpracovává stavový kód a odpověď\nAsync, volání nezpracovává ani stavový kód, ani odpověď, ale může být spuštěno paralelně s jinými cíli", "ENDPOINT": "Koncový bod", "ENDPOINT_DESCRIPTION": "Zadejte koncový bod, kde je hostován váš kód. Ujistěte se, že je pro nás přístupný!", "TIMEOUT": "Časový limit", @@ -1508,7 +1511,8 @@ "APPEARANCE": "Vzhled", "OTHER": "Ostatní", "STORAGE": "Data" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index cb973006e4..3993674992 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flows", "DESCRIPTION": "Wähle einen Authentifizierungsflow und löse deine Aktionen bei einem spezifischen Ereignis innerhalb dieses Flows aus." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, eine neue und verbesserte Version von Actions, ist jetzt verfügbar. Die aktuelle Version ist weiterhin zugänglich, aber unsere zukünftige Entwicklung wird sich auf die neue Version konzentrieren, die schließlich die aktuelle ersetzen wird." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "Anwenden" }, "ACTIONSTWO": { + "BETA_NOTE": "Sie verwenden derzeit die neuen Actions V2, die sich in der Beta-Phase befinden. Version 1 ist weiterhin verfügbar, wird jedoch in Zukunft eingestellt. Bitte melden Sie Probleme oder Feedback.", "EXECUTION": { "TITLE": "Aktionen", "DESCRIPTION": "Aktionen ermöglichen es Ihnen, benutzerdefinierten Code als Reaktion auf API-Anfragen, Ereignisse oder bestimmte Funktionen auszuführen. Verwenden Sie sie, um Zitadel zu erweitern, Arbeitsabläufe zu automatisieren und sich in andere Systeme zu integrieren.", @@ -619,6 +621,7 @@ "restCall": "REST Aufruf", "restAsync": "REST Asynchron" }, + "TYPES_DESCRIPTION": "Webhook, der Aufruf verarbeitet den Statuscode, aber die Antwort ist irrelevant\nCall, der Aufruf verarbeitet den Statuscode und die Antwort\nAsync, der Aufruf verarbeitet weder Statuscode noch Antwort, kann aber parallel zu anderen Zielen aufgerufen werden", "ENDPOINT": "Endpunkt", "ENDPOINT_DESCRIPTION": "Geben Sie den Endpunkt ein, an dem Ihr Code gehostet wird. Stellen Sie sicher, dass er für uns zugänglich ist!", "TIMEOUT": "Timeout", @@ -1508,7 +1511,8 @@ "APPEARANCE": "Erscheinungsbild", "OTHER": "Anderes", "STORAGE": "Speicher" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index be0a3d3f17..fd81bfd353 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flows", "DESCRIPTION": "Choose an authentication flow and trigger your action on a specific event within this flow." - } + }, + "ACTIONSTWO_NOTE": "Actions V2 a new, improved version of Actions is now available. The current version is still accessible, but our future development will focus on the new one, which will eventually replace the current version." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "Apply" }, "ACTIONSTWO": { + "BETA_NOTE": "You are currently using the new Actions V2, which is in beta. The previous Version 1 is still available but will be discontinued in the future. Please report any issues or feedback.", "EXECUTION": { "TITLE": "Actions", "DESCRIPTION": "Actions let you run custom code in response to API requests, events or specific functions. Use them to extend Zitadel, automate workflows, and itegrate with other systems.", @@ -619,6 +621,7 @@ "restCall": "REST Call", "restAsync": "REST Async" }, + "TYPES_DESCRIPTION": "Webhook, the call handles the status code but response is irrelevant\nCall, the call handles the status code and response\nAsync, the call handles neither status code nor response, but can be called in parallel with other Targets", "ENDPOINT": "Endpoint", "ENDPOINT_DESCRIPTION": "Enter the endpoint where your code is hosted. Make sure it is accessible to us!", "TIMEOUT": "Timeout", @@ -1511,7 +1514,8 @@ "OTHER": "Other", "STORAGE": "Storage", "ACTIONS": "Actions" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 0fc95241af..aec024eacb 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flujos", "DESCRIPTION": "Elige un flujo de autenticación y activa tu acción en un evento específico dentro de este flujo." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, una nueva y mejorada versión de Actions, ya está disponible. La versión actual sigue siendo accesible, pero nuestro desarrollo futuro se centrará en la nueva, que acabará reemplazando la versión actual." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "Aplicar" }, "ACTIONSTWO": { + "BETA_NOTE": "Actualmente estás usando la nueva versión Actions V2, que está en fase beta. La versión anterior 1 todavía está disponible, pero será descontinuada en el futuro. Por favor, informa de cualquier problema o comentario.", "EXECUTION": { "TITLE": "Acciones", "DESCRIPTION": "Las acciones te permiten ejecutar código personalizado en respuesta a solicitudes de API, eventos o funciones específicas. Úsalas para extender Zitadel, automatizar flujos de trabajo e integrarte con otros sistemas.", @@ -619,6 +621,7 @@ "restCall": "Llamada REST", "restAsync": "REST Asíncrono" }, + "TYPES_DESCRIPTION": "Webhook, la llamada maneja el código de estado pero la respuesta es irrelevante\nCall, la llamada maneja el código de estado y la respuesta\nAsync, la llamada no maneja ni el código de estado ni la respuesta, pero puede ser llamada en paralelo con otros objetivos", "ENDPOINT": "Punto de conexión", "ENDPOINT_DESCRIPTION": "Introduce el punto de conexión donde se aloja tu código. ¡Asegúrate de que sea accesible para nosotros!", "TIMEOUT": "Tiempo de espera", @@ -1509,7 +1512,8 @@ "APPEARANCE": "Apariencia", "OTHER": "Otros", "STORAGE": "Datos" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 60a0a3e482..05e34ad846 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flux", "DESCRIPTION": "Choisissez un flux d'authentification et déclenchez votre action sur un événement spécifique dans ce flux." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, une nouvelle version améliorée de Actions, est désormais disponible. La version actuelle reste accessible, mais notre développement futur se concentrera sur la nouvelle, qui finira par remplacer la version actuelle." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "Appliquer" }, "ACTIONSTWO": { + "BETA_NOTE": "Vous utilisez actuellement la nouvelle version Actions V2, qui est en phase bêta. L'ancienne version 1 est toujours disponible mais sera arrêtée à l'avenir. Veuillez signaler tout problème ou commentaire.", "EXECUTION": { "TITLE": "Actions", "DESCRIPTION": "Les actions vous permettent d'exécuter du code personnalisé en réponse à des requêtes API, des événements ou des fonctions spécifiques. Utilisez-les pour étendre Zitadel, automatiser les flux de travail et vous intégrer à d'autres systèmes.", @@ -619,6 +621,7 @@ "restCall": "Appel REST", "restAsync": "REST Asynchrone" }, + "TYPES_DESCRIPTION": "Webhook, l'appel gère le code d'état mais la réponse est sans importance\nCall, l'appel gère le code d'état et la réponse\nAsync, l'appel ne gère ni le code d'état ni la réponse, mais peut être appelé en parallèle avec d'autres cibles", "ENDPOINT": "Point de terminaison", "ENDPOINT_DESCRIPTION": "Entrez le point de terminaison où votre code est hébergé. Assurez-vous qu'il nous est accessible !", "TIMEOUT": "Délai d'attente", @@ -1508,7 +1511,8 @@ "APPEARANCE": "Apparence", "OTHER": "Autres", "STORAGE": "Stockage" - } + }, + "BETA": "BÊTA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json index 5724c45a51..d46bc96153 100644 --- a/console/src/assets/i18n/hu.json +++ b/console/src/assets/i18n/hu.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Folyamatok", "DESCRIPTION": "Válassz egy hitelesítési folyamatot, és váltasd ki a műveletedet egy adott esemény bekövetkezésekor ebben a folyamatban." - } + }, + "ACTIONSTWO_NOTE": "Az Actions V2, az Actions új, továbbfejlesztett verziója mostantól elérhető. A jelenlegi verzió továbbra is elérhető, de a jövőbeli fejlesztéseink az új verzióra összpontosítanak, amely végül felváltja a jelenlegi verziót." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "Alkalmaz" }, "ACTIONSTWO": { + "BETA_NOTE": "Jelenleg az új Actions V2-t használja, amely béta verzióban van. Az előző 1-es verzió továbbra is elérhető, de a jövőben megszűnik. Kérjük, jelezze az esetleges problémákat vagy visszajelzéseit.", "EXECUTION": { "TITLE": "Műveletek", "DESCRIPTION": "A műveletek lehetővé teszik egyedi kód futtatását API-kérésekre, eseményekre vagy konkrét függvényekre válaszul. Használja őket a Zitadel kiterjesztéséhez, a munkafolyamatok automatizálásához és más rendszerekkel való integrációhoz.", @@ -619,6 +621,7 @@ "restCall": "REST Hívás", "restAsync": "REST Aszinkron" }, + "TYPES_DESCRIPTION": "Webhook, a hívás kezeli az állapotkódot, de a válasz lényegtelen\nCall, a hívás kezeli az állapotkódot és a választ\nAsync, a hívás sem az állapotkódot, sem a választ nem kezeli, de párhuzamosan hívható más célokkal", "ENDPOINT": "Végpont", "ENDPOINT_DESCRIPTION": "Adja meg azt a végpontot, ahol a kódja található. Győződjön meg arról, hogy elérhető számunkra!", "TIMEOUT": "Időtúllépés", @@ -1508,7 +1511,8 @@ "APPEARANCE": "Megjelenés", "OTHER": "Egyéb", "STORAGE": "Tárolás" - } + }, + "BETA": "BÉTA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json index 77584e883b..f8831a224d 100644 --- a/console/src/assets/i18n/id.json +++ b/console/src/assets/i18n/id.json @@ -69,7 +69,8 @@ "FLOWS": { "TITLE": "Mengalir", "DESCRIPTION": "Pilih alur autentikasi dan picu tindakan Anda pada peristiwa tertentu dalam alur ini." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, versi baru dan lebih baik dari Actions, sekarang tersedia. Versi saat ini masih dapat diakses, tetapi pengembangan di masa depan akan difokuskan pada versi baru ini yang pada akhirnya akan menggantikan versi saat ini." }, "SETTINGS": { "INSTANCE": { @@ -496,6 +497,7 @@ "APPLY": "Menerapkan" }, "ACTIONSTWO": { + "BETA_NOTE": "Anda saat ini menggunakan Actions V2 baru, yang masih dalam versi beta. Versi sebelumnya, Versi 1, masih tersedia tetapi akan dihentikan di masa depan. Silakan laporkan masalah atau berikan masukan.", "EXECUTION": { "TITLE": "Tindakan", "DESCRIPTION": "Tindakan memungkinkan Anda menjalankan kode khusus sebagai respons terhadap permintaan API, peristiwa, atau fungsi tertentu. Gunakan ini untuk memperluas Zitadel, mengotomatiskan alur kerja, dan berintegrasi dengan sistem lain.", @@ -586,6 +588,7 @@ "restCall": "Panggilan REST", "restAsync": "REST Asinkron" }, + "TYPES_DESCRIPTION": "Webhook, panggilan menangani kode status tetapi respons tidak relevan\nCall, panggilan menangani kode status dan respons\nAsync, panggilan tidak menangani kode status maupun respons, tetapi dapat dipanggil secara paralel dengan Target lain", "ENDPOINT": "Titik Akhir", "ENDPOINT_DESCRIPTION": "Masukkan titik akhir tempat kode Anda dihosting. Pastikan dapat diakses oleh kami!", "TIMEOUT": "Batas Waktu", @@ -1386,7 +1389,8 @@ "APPEARANCE": "Penampilan", "OTHER": "Lainnya", "STORAGE": "Penyimpanan" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index b01762b175..5dad683bca 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flussi", "DESCRIPTION": "Scegli un flusso di autenticazione e attiva la tua azione su un evento specifico all'interno di questo flusso." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, una nuova versione migliorata di Actions, è ora disponibile. La versione attuale è ancora accessibile, ma i futuri sviluppi si concentreranno su quella nuova, che alla fine sostituirà la versione corrente." }, "SETTINGS": { "INSTANCE": { @@ -528,6 +529,7 @@ "APPLY": "Applicare" }, "ACTIONSTWO": { + "BETA_NOTE": "Stai attualmente utilizzando la nuova versione Actions V2, che è in beta. La precedente Versione 1 è ancora disponibile, ma sarà dismessa in futuro. Ti preghiamo di segnalare eventuali problemi o feedback.", "EXECUTION": { "TITLE": "Azioni", "DESCRIPTION": "Le azioni consentono di eseguire codice personalizzato in risposta a richieste API, eventi o funzioni specifiche. Usale per estendere Zitadel, automatizzare i flussi di lavoro e integrarti con altri sistemi.", @@ -618,6 +620,7 @@ "restCall": "Chiamata REST", "restAsync": "REST Asincrono" }, + "TYPES_DESCRIPTION": "Webhook, la chiamata gestisce il codice di stato ma la risposta è irrilevante\nCall, la chiamata gestisce il codice di stato e la risposta\nAsync, la chiamata non gestisce né il codice di stato né la risposta, ma può essere eseguita in parallelo con altri obiettivi", "ENDPOINT": "Endpoint", "ENDPOINT_DESCRIPTION": "Inserisci l'endpoint in cui è ospitato il tuo codice. Assicurati che sia accessibile per noi!", "TIMEOUT": "Timeout", @@ -1508,7 +1511,8 @@ "APPEARANCE": "Aspetto", "OTHER": "Altro", "STORAGE": "Dati" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index c0429ec816..f09dfeb564 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "フロー", "DESCRIPTION": "認証フローを選択し、そのフロー内の特定のイベントでアクションをトリガーします。" - } + }, + "ACTIONSTWO_NOTE": "Actions V2(アクションズV2)、改善された新しいバージョンが利用可能になりました。現在のバージョンも引き続き利用可能ですが、今後の開発は新バージョンに集中し、最終的には現在のバージョンを置き換える予定です。" }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "アプライ" }, "ACTIONSTWO": { + "BETA_NOTE": "現在、新しいActions V2(ベータ版)を使用しています。以前のバージョン1はまだ利用可能ですが、今後廃止される予定です。問題やフィードバックがあればお知らせください。", "EXECUTION": { "TITLE": "アクション", "DESCRIPTION": "アクションを使用すると、APIリクエスト、イベント、または特定の関数に応答してカスタムコードを実行できます。これらを使用して、Zitadelを拡張し、ワークフローを自動化し、他のシステムと統合します。", @@ -619,6 +621,7 @@ "restCall": "REST 呼び出し", "restAsync": "REST 非同期" }, + "TYPES_DESCRIPTION": "Webhook、呼び出しはステータスコードを処理しますが、応答は無関係です\nCall、呼び出しはステータスコードと応答を処理します\nAsync、呼び出しはステータスコードも応答も処理しませんが、他のターゲットと並行して呼び出すことができます", "ENDPOINT": "エンドポイント", "ENDPOINT_DESCRIPTION": "コードがホストされているエンドポイントを入力します。アクセス可能であることを確認してください。", "TIMEOUT": "タイムアウト", @@ -1508,7 +1511,8 @@ "APPEARANCE": "設定", "OTHER": "その他", "STORAGE": "ストレージ" - } + }, + "BETA": "ベータ" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json index b791234cd5..304f52e127 100644 --- a/console/src/assets/i18n/ko.json +++ b/console/src/assets/i18n/ko.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "플로우", "DESCRIPTION": "인증 플로우를 선택하고 이 플로우 내의 특정 이벤트에서 작업을 트리거하세요." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, 개선된 새로운 버전이 출시되었습니다. 현재 버전은 여전히 접근할 수 있지만, 앞으로의 개발은 새로운 버전에 집중될 것이며, 결국 현재 버전을 대체할 것입니다." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "적용" }, "ACTIONSTWO": { + "BETA_NOTE": "현재 베타 버전인 새로운 Actions V2를 사용하고 있습니다. 이전 버전 1은 여전히 사용 가능하지만, 향후 중단될 예정입니다. 문제나 피드백이 있으면 알려주세요.", "EXECUTION": { "TITLE": "작업", "DESCRIPTION": "작업을 통해 API 요청, 이벤트 또는 특정 함수에 대한 응답으로 사용자 지정 코드를 실행할 수 있습니다. 이를 사용하여 Zitadel을 확장하고 워크플로를 자동화하며 다른 시스템과 통합합니다.", @@ -619,6 +621,7 @@ "restCall": "REST 호출", "restAsync": "REST 비동기" }, + "TYPES_DESCRIPTION": "Webhook, 호출은 상태 코드를 처리하지만 응답은 중요하지 않습니다\nCall, 호출은 상태 코드와 응답을 처리합니다\nAsync, 호출은 상태 코드나 응답을 처리하지 않지만 다른 대상과 병렬로 호출할 수 있습니다", "ENDPOINT": "엔드포인트", "ENDPOINT_DESCRIPTION": "코드가 호스팅되는 엔드포인트를 입력하십시오. 우리에게 액세스할 수 있는지 확인하십시오!", "TIMEOUT": "시간 초과", @@ -1508,7 +1511,8 @@ "APPEARANCE": "외형", "OTHER": "기타", "STORAGE": "저장소" - } + }, + "BETA": "베타" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 22e0e6d3d7..1ab4dce534 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Текови", "DESCRIPTION": "Изберете тек на автентификација и активирајте ја вашата акција на специфичен настан во тој тек." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, нова и подобрена верзија на Actions, сега е достапна. Сегашната верзија сè уште е достапна, но идниот развој ќе биде насочен кон новата верзија, која на крајот ќе ја замени сегашната." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "Пријавете се" }, "ACTIONSTWO": { + "BETA_NOTE": "", "EXECUTION": { "TITLE": "Акции", "DESCRIPTION": "Акциите ви овозможуваат да извршувате прилагоден код како одговор на API барања, настани или специфични функции. Користете ги за да го проширите Zitadel, да ги автоматизирате работните процеси и да се интегрирате со други системи.", @@ -619,6 +621,7 @@ "restCall": "REST Повик", "restAsync": "REST Асинхроно" }, + "TYPES_DESCRIPTION": "Webhook, повикот го обработува статусниот код но одговорот е ирелевантен\nCall, повикот го обработува статусниот код и одговорот\nAsync, повикот не го обработува ниту статусниот код ниту одговорот, но може да се повика паралелно со други цели", "ENDPOINT": "Крајна точка", "ENDPOINT_DESCRIPTION": "Внесете ја крајната точка каде што е хостиран вашиот код. Осигурете се дека е достапна за нас!", "TIMEOUT": "Време на истекување", @@ -1509,7 +1512,8 @@ "APPEARANCE": "Изглед", "OTHER": "Друго", "STORAGE": "складирање" - } + }, + "BETA": "БЕТА" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index 112474e770..a08f62ed20 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Stromen", "DESCRIPTION": "Kies een authenticatiestroom en activeer je actie bij een specifieke gebeurtenis binnen deze stroom." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, een nieuwe en verbeterde versie van Actions, is nu beschikbaar. De huidige versie blijft toegankelijk, maar onze toekomstige ontwikkeling zal zich richten op de nieuwe versie, die uiteindelijk de huidige zal vervangen." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "Toepassen" }, "ACTIONSTWO": { + "BETA_NOTE": "U gebruikt momenteel de nieuwe Actions V2, die zich in de bètaversie bevindt. De vorige versie 1 is nog beschikbaar maar zal in de toekomst worden stopgezet. Meld alstublieft eventuele problemen of feedback.", "EXECUTION": { "TITLE": "Acties", "DESCRIPTION": "Met acties kunt u aangepaste code uitvoeren als reactie op API-verzoeken, gebeurtenissen of specifieke functies. Gebruik ze om Zitadel uit te breiden, workflows te automatiseren en te integreren met andere systemen.", @@ -619,6 +621,7 @@ "restCall": "REST Aanroep", "restAsync": "REST Asynchroon" }, + "TYPES_DESCRIPTION": "Webhook, de oproep verwerkt de statuscode maar de reactie is irrelevant\nCall, de oproep verwerkt de statuscode en de reactie\nAsync, de oproep verwerkt noch de statuscode noch de reactie, maar kan parallel aan andere doelen worden aangeroepen", "ENDPOINT": "Eindpunt", "ENDPOINT_DESCRIPTION": "Voer het eindpunt in waar uw code wordt gehost. Zorg ervoor dat het voor ons toegankelijk is!", "TIMEOUT": "Time-out", @@ -1508,7 +1511,8 @@ "APPEARANCE": "Verschijning", "OTHER": "Andere", "STORAGE": "opslag" - } + }, + "BETA": "BÈTA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 3244ccb4a6..3902378af6 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Przepływy", "DESCRIPTION": "Wybierz przepływ uwierzytelniania i wywołaj swoją akcję przy określonym zdarzeniu w tym przepływie." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, nowa, ulepszona wersja Actions, jest już dostępna. Obecna wersja jest nadal dostępna, ale przyszły rozwój będzie skoncentrowany na nowej wersji, która ostatecznie zastąpi obecną." }, "SETTINGS": { "INSTANCE": { @@ -528,6 +529,7 @@ "APPLY": "Stosować" }, "ACTIONSTWO": { + "BETA_NOTE": "Obecnie korzystasz z nowej wersji Actions V2, która jest w fazie beta. Poprzednia wersja 1 jest nadal dostępna, ale w przyszłości zostanie wycofana. Prosimy o zgłaszanie wszelkich problemów lub opinii.", "EXECUTION": { "TITLE": "Akcje", "DESCRIPTION": "Akcje umożliwiają uruchamianie niestandardowego kodu w odpowiedzi na żądania API, zdarzenia lub określone funkcje. Użyj ich, aby rozszerzyć Zitadel, zautomatyzować przepływy pracy i zintegrować się z innymi systemami.", @@ -618,6 +620,7 @@ "restCall": "Wywołanie REST", "restAsync": "REST Asynchroniczny" }, + "TYPES_DESCRIPTION": "Webhook, wywołanie obsługuje kod stanu, ale odpowiedź jest nieistotna\nCall, wywołanie obsługuje kod stanu i odpowiedź\nAsync, wywołanie nie obsługuje ani kodu stanu, ani odpowiedzi, ale może być wywoływane równolegle z innymi celami", "ENDPOINT": "Punkt końcowy", "ENDPOINT_DESCRIPTION": "Wprowadź punkt końcowy, w którym hostowany jest Twój kod. Upewnij się, że jest dla nas dostępny!", "TIMEOUT": "Limit czasu", @@ -1507,7 +1510,8 @@ "APPEARANCE": "Wygląd", "OTHER": "Inne", "STORAGE": "składowanie" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index 30b0f1d4e8..016785f2c8 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Fluxos", "DESCRIPTION": "Escolha um fluxo de autenticação e acione sua ação em um evento específico dentro desse fluxo." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, uma nova e melhorada versão de Actions, já está disponível. A versão atual ainda é acessível, mas o nosso desenvolvimento futuro se concentrará na nova versão, que acabará por substituir a atual." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "Aplicar" }, "ACTIONSTWO": { + "BETA_NOTE": "Você está atualmente usando a nova Actions V2, que está em versão beta. A versão anterior 1 ainda está disponível, mas será descontinuada no futuro. Por favor, reporte quaisquer problemas ou envie feedback.", "EXECUTION": { "TITLE": "Ações", "DESCRIPTION": "As ações permitem que você execute código personalizado em resposta a solicitações de API, eventos ou funções específicas. Use-as para estender o Zitadel, automatizar fluxos de trabalho e integrar-se a outros sistemas.", @@ -619,6 +621,7 @@ "restCall": "Chamada REST", "restAsync": "REST Assíncrono" }, + "TYPES_DESCRIPTION": "Webhook, a chamada lida com o código de status, mas a resposta é irrelevante\nCall, a chamada lida com o código de status e a resposta\nAsync, a chamada não lida nem com o código de status nem com a resposta, mas pode ser chamada em paralelo com outros alvos", "ENDPOINT": "Ponto de Extremidade", "ENDPOINT_DESCRIPTION": "Insira o ponto de extremidade onde seu código está hospedado. Certifique-se de que ele esteja acessível para nós!", "TIMEOUT": "Tempo Limite", @@ -1509,7 +1512,8 @@ "APPEARANCE": "Aparência", "OTHER": "Outro", "STORAGE": "armazenar" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json index 6c73852beb..4ad511c466 100644 --- a/console/src/assets/i18n/ro.json +++ b/console/src/assets/i18n/ro.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Fluxuri", "DESCRIPTION": "Alegeți un flux de autentificare și declanșați acțiunea dvs. la un anumit eveniment din cadrul acestui flux." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, o nouă versiune îmbunătățită a Actions, este acum disponibilă. Versiunea actuală este încă accesibilă, dar dezvoltarea viitoare se va concentra pe cea nouă, care în cele din urmă va înlocui versiunea actuală." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "Aplicați" }, "ACTIONSTWO": { + "BETA_NOTE": "În prezent utilizați noua versiune Actions V2, care este în faza beta. Versiunea anterioară 1 este încă disponibilă, dar va fi întreruptă în viitor. Vă rugăm să raportați orice problemă sau feedback.", "EXECUTION": { "TITLE": "Acțiuni", "DESCRIPTION": "Acțiunile vă permit să rulați cod personalizat ca răspuns la cereri API, evenimente sau funcții specifice. Folosiți-le pentru a extinde Zitadel, a automatiza fluxurile de lucru și a vă integra cu alte sisteme.", @@ -619,6 +621,7 @@ "restCall": "Apel REST", "restAsync": "REST Asincron" }, + "TYPES_DESCRIPTION": "Webhook, apelul gestionează codul de stare, dar răspunsul este irelevant\nCall, apelul gestionează codul de stare și răspunsul\nAsync, apelul nu gestionează nici codul de stare, nici răspunsul, dar poate fi apelat în paralel cu alte Ținte", "ENDPOINT": "Punct Final", "ENDPOINT_DESCRIPTION": "Introduceți punctul final unde este găzduit codul dvs. Asigurați-vă că este accesibil pentru noi!", "TIMEOUT": "Timeout", @@ -1506,7 +1509,8 @@ "APPEARANCE": "Aspect", "OTHER": "Altele", "STORAGE": "Stocare" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index 6e2d7df7b4..43bc266be0 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Потоки", "DESCRIPTION": "Выберите поток аутентификации и активируйте ваше действие на определенном событии в этом потоке." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, новая и улучшенная версия Actions, теперь доступна. Текущая версия всё ещё доступна, но дальнейшая разработка будет сосредоточена на новой версии, которая в конечном итоге заменит текущую." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "Применять" }, "ACTIONSTWO": { + "BETA_NOTE": "Вы используете новую версию Actions V2, которая находится в бета-тестировании. Предыдущая версия 1 всё ещё доступна, но будет отключена в будущем. Пожалуйста, сообщайте о любых проблемах или отправляйте отзывы.", "EXECUTION": { "TITLE": "Действия", "DESCRIPTION": "Действия позволяют запускать пользовательский код в ответ на API-запросы, события или определенные функции. Используйте их для расширения Zitadel, автоматизации рабочих процессов и интеграции с другими системами.", @@ -619,6 +621,7 @@ "restCall": "REST Вызов", "restAsync": "REST Асинхронный" }, + "TYPES_DESCRIPTION": "Webhook, вызов обрабатывает код состояния, но ответ не имеет значения\nCall, вызов обрабатывает код состояния и ответ\nAsync, вызов не обрабатывает ни код состояния, ни ответ, но может выполняться параллельно с другими целями", "ENDPOINT": "Конечная точка", "ENDPOINT_DESCRIPTION": "Введите конечную точку, где размещен ваш код. Убедитесь, что он доступен для нас!", "TIMEOUT": "Тайм-аут", @@ -1553,7 +1556,8 @@ "APPEARANCE": "Вид", "OTHER": "Другое", "STORAGE": "хранилище" - } + }, + "BETA": "БЕТА" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index e747571f7a..00b7854603 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "Flöden", "DESCRIPTION": "Välj ett autentiseringsflöde och trigga din åtgärd vid en specifik händelse inom detta flöde." - } + }, + "ACTIONSTWO_NOTE": "Actions V2, en ny och förbättrad version av Actions, är nu tillgänglig. Den nuvarande versionen är fortfarande tillgänglig, men framtida utveckling kommer att fokusera på den nya, som så småningom kommer att ersätta den nuvarande." }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "Tillämpa" }, "ACTIONSTWO": { + "BETA_NOTE": "Du använder för närvarande nya Actions V2, som är i betaversion. Den tidigare versionen 1 är fortfarande tillgänglig men kommer att avvecklas i framtiden. Vänligen rapportera eventuella problem eller ge feedback.", "EXECUTION": { "TITLE": "Åtgärder", "DESCRIPTION": "Åtgärder låter dig köra anpassad kod som svar på API-förfrågningar, händelser eller specifika funktioner. Använd dem för att utöka Zitadel, automatisera arbetsflöden och integrera med andra system.", @@ -619,6 +621,7 @@ "restCall": "REST Anrop", "restAsync": "REST Asynkron" }, + "TYPES_DESCRIPTION": "Webhook, anropet hanterar statuskoden men svaret är irrelevant\nCall, anropet hanterar statuskoden och svaret\nAsync, anropet hanterar varken statuskod eller svar men kan anropas parallellt med andra mål", "ENDPOINT": "Slutpunkt", "ENDPOINT_DESCRIPTION": "Ange slutpunkten där din kod finns. Se till att den är tillgänglig för oss!", "TIMEOUT": "Tidsgräns", @@ -1512,7 +1515,8 @@ "APPEARANCE": "Utseende", "OTHER": "Övrigt", "STORAGE": "Lagring" - } + }, + "BETA": "BETA" }, "SETTING": { "LANGUAGES": { diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 945e4200ef..496b3d528e 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -75,7 +75,8 @@ "FLOWS": { "TITLE": "流程", "DESCRIPTION": "选择一个认证流程,并在该流程中的特定事件上触发您的操作。" - } + }, + "ACTIONSTWO_NOTE": "Actions V2,一个全新改进版的Actions,现在已上线。目前版本仍可使用,但未来开发将专注于新版本,最终将取代当前版本。" }, "SETTINGS": { "INSTANCE": { @@ -529,6 +530,7 @@ "APPLY": "申请" }, "ACTIONSTWO": { + "BETA_NOTE": "您目前正在使用新的 Actions V2(测试版)。之前的版本1仍可使用,但未来将停止支持。请报告任何问题或反馈意见。", "EXECUTION": { "TITLE": "操作", "DESCRIPTION": "操作允许您运行自定义代码以响应 API 请求、事件或特定函数。使用它们来扩展 Zitadel、自动化工作流程并与其他系统集成。", @@ -619,6 +621,7 @@ "restCall": "REST 调用", "restAsync": "REST 异步" }, + "TYPES_DESCRIPTION": "Webhook,调用处理状态码但响应无关紧要\nCall,调用处理状态码和响应\nAsync,调用既不处理状态码也不处理响应,但可以与其他目标并行调用", "ENDPOINT": "端点", "ENDPOINT_DESCRIPTION": "输入您的代码托管的端点。确保我们可以访问它!", "TIMEOUT": "超时", @@ -1508,7 +1511,8 @@ "APPEARANCE": "外观", "OTHER": "其他", "STORAGE": "贮存" - } + }, + "BETA": "测试版" }, "SETTING": { "LANGUAGES": { diff --git a/console/yarn.lock b/console/yarn.lock index 5dd602dfa8..2e586abedb 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -3452,29 +3452,22 @@ js-yaml "^3.10.0" tslib "^2.4.0" -"@zitadel/client@^1.0.7": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.0.7.tgz#39dc8d3d10bfa01e5cf56205ba188f79c39f052d" - integrity sha512-sZG4NEa8vQBt3+4W1AesY+5DstDBuZiqGH2EM+UqbO5D93dlDZInXqZ5oRE7RSl2Bk5ED9mbMFrB7b8DuRw72A== +"@zitadel/client@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.2.0.tgz#8cdc3090f75fcf3a78c4f0266d3c56a0cca6821a" + integrity sha512-Q20nXhKD7VDb8D1UxhDxubC70GFrSPckrJviPR/rAfRR5slUIRTk3AvDS6Q1WvUn4Xtt+btnq52Z5O8lZtVG0w== dependencies: "@bufbuild/protobuf" "^2.2.2" "@connectrpc/connect" "^2.0.0" "@connectrpc/connect-node" "^2.0.0" "@connectrpc/connect-web" "^2.0.0" - "@zitadel/proto" "1.0.4" + "@zitadel/proto" "1.2.0" jose "^5.3.0" -"@zitadel/proto@1.0.4": - 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== +"@zitadel/proto@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.2.0.tgz#9b9a40defcd9e8464627cc99ac3fd7bcf8994ffd" + integrity sha512-OqHgyCnD9l950xswdVNPIsLA01qSpOPf+0bYqYJWHafytIBbvGNJRnypu4X0LnaFXLM6LakkP4pWYeiGLmwxaw== dependencies: "@bufbuild/protobuf" "^2.2.2" From c36b0ab2e2a0a2632cffae4fe6ac086a126bc2a8 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Tue, 29 Apr 2025 16:12:34 +0200 Subject: [PATCH 12/26] docs(self-hosting): add login to lb example (#9496) # Which Problems Are Solved We have no docs for self-hosting the login using the standard login as a standalone docker container. # How the Problems Are Solved A common self-hosting case is to publish the login at the same domain as Zitadel behind a reverse proxy. That's why we extend the load balancing example. We refocus the example from *making TLS work* to *running multiple services behind the proxy and connect them using an internal network and DNS*. I decided this together with @fforootd. For authenticating with the login application, we have to set up a service user and give it the role IAM_LOGIN_CLIENT. We do so in the use-new-login "job" container as `zitadel setup` only supports Zitadel users with the role IAM_ADMIN AFAIR. The login application relies on a healthy Zitadel API on startup, which is why we fix the containers readiness reports. # Additional Changes - We deploy the init and setup jobs independently, because this better reflects our production recommendatinons. It gives more control over the upgrade process. - We use the ExternalDomain *127.0.0.1.sslip.io* instead of *my.domain*, because this doesn't require changing the local DNS resolution by changing */etc/hosts* for local tests. # Testing The commands in the preview docs use to the configuration files on main. This is fine when the PR is merged but not for testing the PR. Replace the used links to make them point to the PRs changed files. Instead of the commands in the preview docs, use these: ```bash # Download the docker compose example configuration. wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml # Download the Traefik example configuration. wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml # Download and adjust the example configuration file containing standard configuration. wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml # Download and adjust the example configuration file containing secret configuration. wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-secrets.yaml # Download and adjust the example configuration file containing database initialization configuration. wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml # A single ZITADEL instance always needs the same 32 bytes long masterkey # Generate one to a file if you haven't done so already and pass it as environment variable LC_ALL=C tr -dc '[:graph:]' ./zitadel-masterkey export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)" # Run the database and application containers docker compose up --detach --wait ``` # Additional Context - Closes https://github.com/zitadel/DevOps/issues/111 - Depends on https://github.com/zitadel/typescript/pull/412 - Contributes to road map item https://github.com/zitadel/zitadel/issues/9481 --- .../deploy/loadbalancing-example/.gitignore | 1 + .../loadbalancing-example/docker-compose.yaml | 153 +++++++++++++++--- .../example-traefik.yaml | 57 ++----- .../example-zitadel-config.yaml | 31 ++-- .../example-zitadel-init-steps.yaml | 12 +- .../loadbalancing-example.mdx | 35 ++-- 6 files changed, 183 insertions(+), 106 deletions(-) create mode 100644 docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore b/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore new file mode 100644 index 0000000000..bd98bacd66 --- /dev/null +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore @@ -0,0 +1 @@ +.env-file 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 d1d8c95bb2..013fc2aa22 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml @@ -1,48 +1,157 @@ services: - traefik: + db: + image: postgres:17-alpine + restart: unless-stopped + environment: + - POSTGRES_USER=root + - POSTGRES_PASSWORD=postgres networks: - - 'zitadel' - image: "traefik:latest" - ports: - - "80:80" - - "443:443" + - 'storage' + healthcheck: + test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"] + interval: 10s + timeout: 60s + retries: 5 + start_period: 10s volumes: - - "./example-traefik.yaml:/etc/traefik/traefik.yaml" + - 'data:/var/lib/postgresql/data:rw' - zitadel: - restart: 'always' + zitadel-init: + restart: 'no' networks: - - 'zitadel' + - 'storage' image: 'ghcr.io/zitadel/zitadel:latest' - command: 'start-from-init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml --steps /example-zitadel-init-steps.yaml --masterkey "${ZITADEL_MASTERKEY}" --tlsMode external' + command: 'init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml' depends_on: db: condition: 'service_healthy' volumes: - './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro' - './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro' + + zitadel-setup: + restart: 'no' + networks: + - 'storage' + # We use the debug image so we have the environment to + # - create the .env file for the login to authenticate at Zitadel + # - set the correct permissions for the .env-file folder + image: 'ghcr.io/zitadel/zitadel:latest-debug' + user: root + entrypoint: '/bin/sh' + command: + - -c + - > + /app/zitadel setup + --config /example-zitadel-config.yaml + --config /example-zitadel-secrets.yaml + --steps /example-zitadel-init-steps.yaml + --masterkey ${ZITADEL_MASTERKEY} && + mv /pat /.env-file/pat || exit 0 && + echo ZITADEL_SERVICE_USER_TOKEN=$(cat /.env-file/pat) > /.env-file/.env && + chown -R 1001:${GID} /.env-file && + chmod -R 770 /.env-file + environment: + - GID + depends_on: + zitadel-init: + condition: 'service_completed_successfully' + restart: false + volumes: + - './.env-file:/.env-file:rw' + - './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro' + - './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro' - './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro' - db: - image: postgres:17-alpine - restart: always - environment: - - POSTGRES_USER=root - - POSTGRES_PASSWORD=postgres + zitadel: + restart: 'unless-stopped' networks: - - 'zitadel' + - 'backend' + - 'storage' + image: 'ghcr.io/zitadel/zitadel:latest' + command: > + start --config /example-zitadel-config.yaml + --config /example-zitadel-secrets.yaml + --masterkey ${ZITADEL_MASTERKEY} + depends_on: + zitadel-setup: + condition: 'service_completed_successfully' + restart: true + volumes: + - './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro' + - './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro' + ports: + - "8080:8080" healthcheck: - test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"] + test: [ + "CMD", "/app/zitadel", "ready", + "--config", "/example-zitadel-config.yaml", + "--config", "/example-zitadel-secrets.yaml" + ] interval: 10s timeout: 60s retries: 5 - start_period: 10s + start_period: 10s + + # The use-new-login service configures Zitadel to use the new login v2 for all applications. + # It also gives the setupped machine user the necessary IAM_LOGIN_CLIENT role. + use-new-login: + restart: 'on-failure' + user: "1001" + networks: + - 'backend' + image: 'badouralix/curl-jq:alpine' + entrypoint: '/bin/sh' + command: + - -c + - > + curl -X PUT -H "Host: 127.0.0.1.sslip.io" -H "Authorization: Bearer $(cat ./.env-file/pat)" --insecure http://zitadel:8080/v2/features/instance -d '{"loginV2": {"required": true}}' && + LOGIN_USER=$(curl --fail-with-body -H "Host: 127.0.0.1.sslip.io" -H "Authorization: Bearer $(cat ./.env-file/pat)" --insecure http://zitadel:8080/auth/v1/users/me | jq -r '.user.id') && + curl -X PUT -H "Host: 127.0.0.1.sslip.io" -H "Authorization: Bearer $(cat ./.env-file/pat)" --insecure http://zitadel:8080/admin/v1/members/$${LOGIN_USER} -d '{"roles": ["IAM_OWNER", "IAM_LOGIN_CLIENT"]}' volumes: - - 'data:/var/lib/postgresql/data:rw' + - './.env-file:/.env-file:ro' + depends_on: + zitadel: + condition: 'service_healthy' + restart: false + + login: + restart: 'unless-stopped' + networks: + - 'backend' + image: 'ghcr.io/zitadel/login:main' + environment: + - ZITADEL_API_URL=http://zitadel:8080 + - CUSTOM_REQUEST_HEADERS=Host:127.0.0.1.sslip.io + - NEXT_PUBLIC_BASE_PATH="/ui/v2/login" + user: "${UID:-1000}" + volumes: + - './.env-file:/.env-file:ro' + depends_on: + zitadel: + condition: 'service_healthy' + restart: false + + traefik: + restart: 'unless-stopped' + networks: + - 'backend' + image: "traefik:latest" + ports: + - "80:80" + - "443:443" + volumes: + - "./example-traefik.yaml:/etc/traefik/traefik.yaml" + depends_on: + zitadel: + condition: 'service_healthy' + login: + condition: 'service_started' networks: - zitadel: + storage: + backend: volumes: data: diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml index c16f74a46d..a3af425172 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml @@ -4,66 +4,37 @@ log: accessLog: {} entrypoints: - web: - address: ":80" - websecure: address: ":443" -tls: - stores: - default: - # generates self-signed certificates - defaultCertificate: - providers: file: filename: /etc/traefik/traefik.yaml http: - middlewares: - zitadel: - headers: - isDevelopment: false - allowedHosts: - - 'my.domain' - customRequestHeaders: - authority: 'my.domain' - redirect-to-https: - redirectScheme: - scheme: https - port: 443 - permanent: true - routers: - # Redirect HTTP to HTTPS - router0: + login: entryPoints: - - web - middlewares: - - redirect-to-https - rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)' - service: zitadel - # The actual ZITADEL router - router1: + - websecure + service: login + rule: 'Host(`127.0.0.1.sslip.io`) && PathPrefix(`/ui/v2/login`)' + tls: {} + zitadel: entryPoints: - websecure service: zitadel - middlewares: - - zitadel - rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)' - tls: - domains: - - main: "my.domain" - sans: - - "*.my.domain" - - "my.domain" + rule: 'Host(`127.0.0.1.sslip.io`) && !PathPrefix(`/ui/v2/login`)' + tls: {} - # Add the service services: + login: + loadBalancer: + servers: + - url: http://login:3000 + passHostHeader: true zitadel: loadBalancer: servers: - # h2c is the scheme for unencrypted HTTP/2 - url: h2c://zitadel:8080 passHostHeader: true + diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml index 392bf1148e..fadd39373d 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml @@ -1,26 +1,29 @@ # All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml -Log: - Level: 'info' -# Make ZITADEL accessible over HTTPs, not HTTP ExternalSecure: true -ExternalDomain: my.domain +ExternalDomain: 127.0.0.1.sslip.io ExternalPort: 443 +# Traefik terminates TLS. Inside the Docker network, we use plain text. +TLS.Enabled: false + # If not using the docker compose example, adjust these values for connecting ZITADEL to your PostgreSQL Database: postgres: Host: 'db' Port: 5432 Database: zitadel - User: - SSL: - Mode: 'disable' - Admin: - SSL: - Mode: 'disable' + User.SSL.Mode: 'disable' + Admin.SSL.Mode: 'disable' -LogStore: - Access: - Stdout: - Enabled: true +# By default, ZITADEL should redirect to /ui/v2/login +OIDC: + DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2 + DefaultLogoutURLV2: "/ui/v2/login/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2 +SAML.DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2 + +# Access logs allow us to debug Network issues +LogStore.Access.Stdout.Enabled: true + +# Skipping the MFA init step allows us to immediately authenticate at the console +DefaultInstance.LoginPolicy.MfaInitSkipLifetime: "0s" diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml index 804e3d18d8..be63164ced 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml @@ -1,8 +1,12 @@ # All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml FirstInstance: + PatPath: '/pat' Org: - Name: 'My Org' + # We want to authenticate immediately at the console without changing the password Human: - # use the loginname root@my-org.my.domain - Username: 'root' - Password: 'RootPassword1!' + PasswordChangeRequired: false + Machine: + Machine: + Username: 'login-container' + Name: 'Login Container' + Pat.ExpirationDate: '2029-01-01T00:00:00Z' 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 d5e3984568..88cd4c7700 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx @@ -1,5 +1,5 @@ --- -title: A ZITADEL Load Balancing Example +title: A Zitadel Load Balancing Example --- import CodeBlock from '@theme/CodeBlock'; @@ -8,16 +8,16 @@ import ExampleTraefikSource from '!!raw-loader!./example-traefik.yaml' import ExampleZITADELConfigSource from '!!raw-loader!./example-zitadel-config.yaml' import ExampleZITADELSecretsSource from '!!raw-loader!./example-zitadel-secrets.yaml' import ExampleZITADELInitStepsSource from '!!raw-loader!./example-zitadel-init-steps.yaml' -import NoteInstanceNotFound from '../troubleshooting/_note_instance_not_found.mdx'; -With this example configuration, you create a near production environment for ZITADEL with [Docker Compose](https://docs.docker.com/compose/). - -The stack consists of three long-running containers: -- A [Traefik](https://doc.traefik.io/traefik/) reverse proxy with upstream HTTP/2 enabled, issuing a self-signed TLS certificate. -- A secure ZITADEL container configured for a custom domain. As we terminate TLS with Traefik, we configure ZITADEL for `--tlsMode external`. +The stack consists of four long-running containers and a couple of short-lived containers: +- A [Traefik](https://doc.traefik.io/traefik/) reverse proxy container with upstream HTTP/2 enabled, issuing a self-signed TLS certificate. +- A Login container that is accessible via Traefik at `/ui/v2/login` +- A Zitadel container that is accessible via Traefik at all other paths than `/ui/v2/login`. - An insecure [PostgreSQL](https://www.postgresql.org/docs/current/index.html). -The setup is tested against Docker version 20.10.17 and Docker Compose version v2.2.3 +The Traefik container and the login container call the Zitadel container via the internal Docker network at `h2c://zitadel:8080` + +The setup is tested against Docker version 28.0.4 and Docker Compose version v2.34.0 By executing the commands below, you will download the following files: @@ -64,22 +64,11 @@ tr -dc A-Za-z0-9 ./zitadel-masterkey export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)" # Run the database and application containers -docker compose up --detach +docker compose up --detach --wait ``` -Make `127.0.0.1` available at `my.domain`. For example, this can be achieved with an entry `127.0.0.1 my.domain` in the `/etc/hosts` file. - -Open your favorite internet browser at [https://my.domain/ui/console/](https://my.domain/ui/console/). -You can safely proceed, if your browser warns you about the insecure self-signed TLS certificate. -This is the IAM admin users login according to your configuration in the [example-zitadel-init-steps.yaml](./example-zitadel-init-steps.yaml): -- **username**: *root@my-org.my.domain* -- **password**: *RootPassword1!* +Open your favorite internet browser at https://127.0.0.1.sslip.io/ui/console?login_hint=zitadel-admin@zitadel.127.0.0.1.sslip.io. +Your browser warns you about the insecure self-signed TLS certificate. As 127.0.0.1.sslip.io resolves to your localhost, you can safely proceed. +Use the password *Password1!* to log in. Read more about [the login process](/guides/integrate/login/oidc/login-users). - - - -## Troubleshooting - -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'` From fa3efd9da3ad998242ad680e803f6c2bb256cb9f Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Tue, 29 Apr 2025 16:33:23 +0200 Subject: [PATCH 13/26] docs: fix Illegal byte sequence (#9750) # Which Problems Are Solved In some docs pages, we propose to generate a Zitadel masterkey using the command `tr -dc A-Za-z0-9 ./zitadel-masterkey +LC_ALL=C tr -dc '[:graph:]' ./zitadel-masterkey export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)" # Run the database and application containers diff --git a/docs/docs/self-hosting/manage/configure/_compose.mdx b/docs/docs/self-hosting/manage/configure/_compose.mdx index 5e8b1c3937..837d4c6e62 100644 --- a/docs/docs/self-hosting/manage/configure/_compose.mdx +++ b/docs/docs/self-hosting/manage/configure/_compose.mdx @@ -43,7 +43,7 @@ wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosti # A single ZITADEL instance always needs the same 32 bytes long masterkey # Generate one to a file if you haven't done so already and pass it as environment variable -tr -dc A-Za-z0-9 ./zitadel-masterkey +LC_ALL=C tr -dc '[:graph:]' ./zitadel-masterkey export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)" # Run the database and application containers diff --git a/docs/docs/self-hosting/manage/configure/_linuxunix.mdx b/docs/docs/self-hosting/manage/configure/_linuxunix.mdx index 6be833caea..65130ea195 100644 --- a/docs/docs/self-hosting/manage/configure/_linuxunix.mdx +++ b/docs/docs/self-hosting/manage/configure/_linuxunix.mdx @@ -35,7 +35,7 @@ wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosti # A single ZITADEL instance always needs the same 32 characters long masterkey # If you haven't done so already, you can generate a new one # The key must be passed as argument -ZITADEL_MASTERKEY="$(tr -dc A-Za-z0-9 ./zitadel-masterkey +LC_ALL=C tr -dc '[:graph:]' ./zitadel-masterkey # Let the zitadel binary read configuration from environment variables zitadel start-from-init --masterkey "${ZITADEL_MASTERKEY}" --tlsMode disabled --masterkeyFile ./zitadel-masterkey From 91bc71db74fbfd6945822418a5fa8df4439f5757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 29 Apr 2025 16:54:53 +0200 Subject: [PATCH 14/26] fix(instance): add web key generation to instance defaults (#9815) # Which Problems Are Solved Webkeys were not generated with new instances when the webkey feature flag was enabled for instance defaults. This would cause a redirect loop with console for new instances on QA / coud. # How the Problems Are Solved - uncomment the webkeys section on defaults.yaml - Fix field naming of webkey config # Additional Changes - Add all available features as comments. - Make the improved performance type enum parsable from the config, untill now they were just ints. - Running of the enumer command created missing enum entries for feature keys. # Additional Context - Needs to be back-ported to v3 / next-rc Co-authored-by: Livio Spring --- cmd/defaults.yaml | 31 +++-- internal/api/grpc/feature/v2/converter.go | 6 +- internal/api/grpc/feature/v2beta/converter.go | 6 +- internal/feature/feature.go | 3 +- .../feature/improvedperformancetype_enumer.go | 106 ++++++++++++++++++ internal/feature/key_enumer.go | 66 +++++------ 6 files changed, 171 insertions(+), 47 deletions(-) create mode 100644 internal/feature/improvedperformancetype_enumer.go diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 55e14bbada..f20fbc03fc 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -959,16 +959,15 @@ DefaultInstance: EmailTemplate: CjwhZG9jdHlwZSBodG1sPgo8aHRtbCB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiIHhtbG5zOm89InVybjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSI+CjxoZWFkPgogIDx0aXRsZT4KCiAgPC90aXRsZT4KICA8IS0tW2lmICFtc29dPjwhLS0+CiAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJJRT1lZGdlIj4KICA8IS0tPCFbZW5kaWZdLS0+CiAgPG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgiPgogIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSI+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KICAgICNvdXRsb29rIGEgeyBwYWRkaW5nOjA7IH0KICAgIGJvZHkgeyBtYXJnaW46MDtwYWRkaW5nOjA7LXdlYmtpdC10ZXh0LXNpemUtYWRqdXN0OjEwMCU7LW1zLXRleHQtc2l6ZS1hZGp1c3Q6MTAwJTsgfQogICAgdGFibGUsIHRkIHsgYm9yZGVyLWNvbGxhcHNlOmNvbGxhcHNlO21zby10YWJsZS1sc3BhY2U6MHB0O21zby10YWJsZS1yc3BhY2U6MHB0OyB9CiAgICBpbWcgeyBib3JkZXI6MDtoZWlnaHQ6YXV0bztsaW5lLWhlaWdodDoxMDAlOyBvdXRsaW5lOm5vbmU7dGV4dC1kZWNvcmF0aW9uOm5vbmU7LW1zLWludGVycG9sYXRpb24tbW9kZTpiaWN1YmljOyB9CiAgICBwIHsgZGlzcGxheTpibG9jazttYXJnaW46MTNweCAwOyB9CiAgPC9zdHlsZT4KICA8IS0tW2lmIG1zb10+CiAgPHhtbD4KICAgIDxvOk9mZmljZURvY3VtZW50U2V0dGluZ3M+CiAgICAgIDxvOkFsbG93UE5HLz4KICAgICAgPG86UGl4ZWxzUGVySW5jaD45NjwvbzpQaXhlbHNQZXJJbmNoPgogICAgPC9vOk9mZmljZURvY3VtZW50U2V0dGluZ3M+CiAgPC94bWw+CiAgPCFbZW5kaWZdLS0+CiAgPCEtLVtpZiBsdGUgbXNvIDExXT4KICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgLm1qLW91dGxvb2stZ3JvdXAtZml4IHsgd2lkdGg6MTAwJSAhaW1wb3J0YW50OyB9CiAgPC9zdHlsZT4KICA8IVtlbmRpZl0tLT4KCgogIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBAbWVkaWEgb25seSBzY3JlZW4gYW5kIChtaW4td2lkdGg6NDgwcHgpIHsKICAgICAgLm1qLWNvbHVtbi1wZXItMTAwIHsgd2lkdGg6MTAwJSAhaW1wb3J0YW50OyBtYXgtd2lkdGg6IDEwMCU7IH0KICAgICAgLm1qLWNvbHVtbi1wZXItNjAgeyB3aWR0aDo2MCUgIWltcG9ydGFudDsgbWF4LXdpZHRoOiA2MCU7IH0KICAgIH0KICA8L3N0eWxlPgoKCiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCgoKICAgIEBtZWRpYSBvbmx5IHNjcmVlbiBhbmQgKG1heC13aWR0aDo0ODBweCkgewogICAgICB0YWJsZS5tai1mdWxsLXdpZHRoLW1vYmlsZSB7IHdpZHRoOiAxMDAlICFpbXBvcnRhbnQ7IH0KICAgICAgdGQubWotZnVsbC13aWR0aC1tb2JpbGUgeyB3aWR0aDogYXV0byAhaW1wb3J0YW50OyB9CiAgICB9CgogIDwvc3R5bGU+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc2hhZG93IGEgewogICAgYm94LXNoYWRvdzogMHB4IDNweCAxcHggLTJweCByZ2JhKDAsIDAsIDAsIDAuMiksIDBweCAycHggMnB4IDBweCByZ2JhKDAsIDAsIDAsIDAuMTQpLCAwcHggMXB4IDVweCAwcHggcmdiYSgwLCAwLCAwLCAwLjEyKTsKICB9PC9zdHlsZT4KCiAge3tpZiAuRm9udFVSTH19CiAgPHN0eWxlPgogICAgQGZvbnQtZmFjZSB7CiAgICAgIGZvbnQtZmFtaWx5OiAne3suRm9udEZhY2VGYW1pbHl9fSc7CiAgICAgIGZvbnQtc3R5bGU6IG5vcm1hbDsKICAgICAgZm9udC1kaXNwbGF5OiBzd2FwOwogICAgICBzcmM6IHVybCh7ey5Gb250VVJMfX0pOwogICAgfQogIDwvc3R5bGU+CiAge3tlbmR9fQoKPC9oZWFkPgo8Ym9keSBzdHlsZT0id29yZC1zcGFjaW5nOm5vcm1hbDsiPgoKCjxkaXYKICAgICAgICBzdHlsZT0iIgo+CgogIDx0YWJsZQogICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJhY2tncm91bmQ6e3suQmFja2dyb3VuZENvbG9yfX07YmFja2dyb3VuZC1jb2xvcjp7ey5CYWNrZ3JvdW5kQ29sb3J9fTt3aWR0aDoxMDAlO2JvcmRlci1yYWRpdXM6MTZweDsiCiAgPgogICAgPHRib2R5PgogICAgPHRyPgogICAgICA8dGQ+CgoKICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIGNsYXNzPSIiIHN0eWxlPSJ3aWR0aDo4MDBweDsiIHdpZHRoPSI4MDAiID48dHI+PHRkIHN0eWxlPSJsaW5lLWhlaWdodDowcHg7Zm9udC1zaXplOjBweDttc28tbGluZS1oZWlnaHQtcnVsZTpleGFjdGx5OyI+PCFbZW5kaWZdLS0+CgoKICAgICAgICA8ZGl2ICBzdHlsZT0ibWFyZ2luOjBweCBhdXRvO2JvcmRlci1yYWRpdXM6MTZweDttYXgtd2lkdGg6ODAwcHg7Ij4KCiAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7Ym9yZGVyLXJhZGl1czoxNnB4OyIKICAgICAgICAgID4KICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iZGlyZWN0aW9uOmx0cjtmb250LXNpemU6MHB4O3BhZGRpbmc6MjBweCAwO3BhZGRpbmctbGVmdDowO3RleHQtYWxpZ246Y2VudGVyOyIKICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgcm9sZT0icHJlc2VudGF0aW9uIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCI+PHRyPjx0ZCBjbGFzcz0iIiB3aWR0aD0iODAwcHgiID48IVtlbmRpZl0tLT4KCiAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICA8dGQ+CgoKICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgY2xhc3M9IiIgc3R5bGU9IndpZHRoOjgwMHB4OyIgd2lkdGg9IjgwMCIgPjx0cj48dGQgc3R5bGU9ImxpbmUtaGVpZ2h0OjBweDtmb250LXNpemU6MHB4O21zby1saW5lLWhlaWdodC1ydWxlOmV4YWN0bHk7Ij48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgICAgPGRpdiAgc3R5bGU9Im1hcmdpbjowcHggYXV0bzttYXgtd2lkdGg6ODAwcHg7Ij4KCiAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJ3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImRpcmVjdGlvbjpsdHI7Zm9udC1zaXplOjBweDtwYWRkaW5nOjA7dGV4dC1hbGlnbjpjZW50ZXI7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgcm9sZT0icHJlc2VudGF0aW9uIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCI+PHRyPjx0ZCBjbGFzcz0iIiBzdHlsZT0id2lkdGg6ODAwcHg7IiA+PCFbZW5kaWZdLS0+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9Im1qLWNvbHVtbi1wZXItMTAwIG1qLW91dGxvb2stZ3JvdXAtZml4IiBzdHlsZT0iZm9udC1zaXplOjA7bGluZS1oZWlnaHQ6MDt0ZXh0LWFsaWduOmxlZnQ7ZGlzcGxheTppbmxpbmUtYmxvY2s7d2lkdGg6MTAwJTtkaXJlY3Rpb246bHRyOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiA+PHRyPjx0ZCBzdHlsZT0idmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjgwMHB4OyIgPjwhW2VuZGlmXS0tPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzcz0ibWotY29sdW1uLXBlci0xMDAgbWotb3V0bG9vay1ncm91cC1maXgiIHN0eWxlPSJmb250LXNpemU6MHB4O3RleHQtYWxpZ246bGVmdDtkaXJlY3Rpb246bHRyO2Rpc3BsYXk6aW5saW5lLWJsb2NrO3ZlcnRpY2FsLWFsaWduOnRvcDt3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHdpZHRoPSIxMDAlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgIHN0eWxlPSJ2ZXJ0aWNhbC1hbGlnbjp0b3A7cGFkZGluZzowOyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7e2lmIC5Mb2dvVVJMfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iIiB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzo1MHB4IDAgMzBweCAwO3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYm9yZGVyLWNvbGxhcHNlOmNvbGxhcHNlO2JvcmRlci1zcGFjaW5nOjBweDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCAgc3R5bGU9IndpZHRoOjE4MHB4OyI+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGltZwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBoZWlnaHQ9ImF1dG8iIHNyYz0ie3suTG9nb1VSTH19IiBzdHlsZT0iYm9yZGVyOjA7Ym9yZGVyLXJhZGl1czo4cHg7ZGlzcGxheTpibG9jaztvdXRsaW5lOm5vbmU7dGV4dC1kZWNvcmF0aW9uOm5vbmU7aGVpZ2h0OmF1dG87d2lkdGg6MTAwJTtmb250LXNpemU6MTNweDsiIHdpZHRoPSIxODAiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAvPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PC90ZD48L3RyPjwvdGFibGU+PCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PC90ZD48L3RyPjwvdGFibGU+PCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KCgogICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PC90ZD48L3RyPjwvdGFibGU+PCFbZW5kaWZdLS0+CgoKICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PHRyPjx0ZCBjbGFzcz0iIiB3aWR0aD0iODAwcHgiID48IVtlbmRpZl0tLT4KCiAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICA8dGQ+CgoKICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgY2xhc3M9IiIgc3R5bGU9IndpZHRoOjgwMHB4OyIgd2lkdGg9IjgwMCIgPjx0cj48dGQgc3R5bGU9ImxpbmUtaGVpZ2h0OjBweDtmb250LXNpemU6MHB4O21zby1saW5lLWhlaWdodC1ydWxlOmV4YWN0bHk7Ij48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgICAgPGRpdiAgc3R5bGU9Im1hcmdpbjowcHggYXV0bzttYXgtd2lkdGg6ODAwcHg7Ij4KCiAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJ3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImRpcmVjdGlvbjpsdHI7Zm9udC1zaXplOjBweDtwYWRkaW5nOjA7dGV4dC1hbGlnbjpjZW50ZXI7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgcm9sZT0icHJlc2VudGF0aW9uIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCI+PHRyPjx0ZCBjbGFzcz0iIiBzdHlsZT0idmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjQ4MHB4OyIgPjwhW2VuZGlmXS0tPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzPSJtai1jb2x1bW4tcGVyLTYwIG1qLW91dGxvb2stZ3JvdXAtZml4IiBzdHlsZT0iZm9udC1zaXplOjBweDt0ZXh0LWFsaWduOmxlZnQ7ZGlyZWN0aW9uOmx0cjtkaXNwbGF5OmlubGluZS1ibG9jazt2ZXJ0aWNhbC1hbGlnbjp0b3A7d2lkdGg6MTAwJTsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCAgc3R5bGU9InZlcnRpY2FsLWFsaWduOnRvcDtwYWRkaW5nOjA7Ij4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iIiB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBzdHlsZT0iZm9udC1zaXplOjBweDtwYWRkaW5nOjEwcHggMjVweDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0eWxlPSJmb250LWZhbWlseTp7ey5Gb250RmFtaWx5fX07Zm9udC1zaXplOjI0cHg7Zm9udC13ZWlnaHQ6NTAwO2xpbmUtaGVpZ2h0OjE7dGV4dC1hbGlnbjpjZW50ZXI7Y29sb3I6e3suRm9udENvbG9yfX07IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID57ey5HcmVldGluZ319PC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MTBweCAyNXB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImZvbnQtZmFtaWx5Ont7LkZvbnRGYW1pbHl9fTtmb250LXNpemU6MTZweDtmb250LXdlaWdodDpsaWdodDtsaW5lLWhlaWdodDoxLjU7dGV4dC1hbGlnbjpjZW50ZXI7Y29sb3I6e3suRm9udENvbG9yfX07IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID57ey5UZXh0fX08L2Rpdj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgoKCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIHZlcnRpY2FsLWFsaWduPSJtaWRkbGUiIGNsYXNzPSJzaGFkb3ciIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MTBweCAyNXB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYm9yZGVyLWNvbGxhcHNlOnNlcGFyYXRlO2xpbmUtaGVpZ2h0OjEwMCU7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYmdjb2xvcj0ie3suUHJpbWFyeUNvbG9yfX0iIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJvcmRlcjpub25lO2JvcmRlci1yYWRpdXM6NnB4O2N1cnNvcjphdXRvO21zby1wYWRkaW5nLWFsdDoxMHB4IDI1cHg7YmFja2dyb3VuZDp7ey5QcmltYXJ5Q29sb3J9fTsiIHZhbGlnbj0ibWlkZGxlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGEKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGhyZWY9Int7LlVSTH19IiByZWw9Im5vb3BlbmVyIG5vcmVmZXJyZXIgbm90cmFjayIgc3R5bGU9ImRpc3BsYXk6aW5saW5lLWJsb2NrO2JhY2tncm91bmQ6e3suUHJpbWFyeUNvbG9yfX07Y29sb3I6I2ZmZmZmZjtmb250LWZhbWlseTp7ey5Gb250RmFtaWx5fX07Zm9udC1zaXplOjE0cHg7Zm9udC13ZWlnaHQ6NTAwO2xpbmUtaGVpZ2h0OjEyMCU7bWFyZ2luOjA7dGV4dC1kZWNvcmF0aW9uOm5vbmU7dGV4dC10cmFuc2Zvcm06bm9uZTtwYWRkaW5nOjEwcHggMjVweDttc28tcGFkZGluZy1hbHQ6MHB4O2JvcmRlci1yYWRpdXM6NnB4OyIgdGFyZ2V0PSJfYmxhbmsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3suQnV0dG9uVGV4dH19CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7e2lmIC5JbmNsdWRlRm9vdGVyfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7cGFkZGluZy10b3A6MjBweDtwYWRkaW5nLXJpZ2h0OjIwcHg7cGFkZGluZy1ib3R0b206MjBweDtwYWRkaW5nLWxlZnQ6MjBweDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxwCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iYm9yZGVyLXRvcDpzb2xpZCAycHggI2RiZGJkYjtmb250LXNpemU6MXB4O21hcmdpbjowcHggYXV0bzt3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9wPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHN0eWxlPSJib3JkZXItdG9wOnNvbGlkIDJweCAjZGJkYmRiO2ZvbnQtc2l6ZToxcHg7bWFyZ2luOjBweCBhdXRvO3dpZHRoOjQ0MHB4OyIgcm9sZT0icHJlc2VudGF0aW9uIiB3aWR0aD0iNDQwcHgiID48dHI+PHRkIHN0eWxlPSJoZWlnaHQ6MDtsaW5lLWhlaWdodDowOyI+ICZuYnNwOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgoKCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxNnB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImZvbnQtZmFtaWx5Ont7LkZvbnRGYW1pbHl9fTtmb250LXNpemU6MTNweDtsaW5lLWhlaWdodDoxO3RleHQtYWxpZ246Y2VudGVyO2NvbG9yOnt7LkZvbnRDb2xvcn19OyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+e3suRm9vdGVyVGV4dH19PC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKCiAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgogICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICA8L2Rpdj4KCgogICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgoKCiAgICAgIDwvdGQ+CiAgICA8L3RyPgogICAgPC90Ym9keT4KICA8L3RhYmxlPgoKPC9kaXY+Cgo8L2JvZHk+CjwvaHRtbD4K # ZITADEL_DEFAULTINSTANCE_EMAILTEMPLATE # WebKeys configures the OIDC token signing keys that are generated when a new instance is created. - # WebKeys are still in alpha, so the config is disabled here. This will prevent generation of keys for now. - # WebKeys: - # Type: "rsa" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_TYPE - # Config: - # Bits: "2048" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_BITS - # Hasher: "sha256" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_HASHER + WebKeys: + Type: "rsa" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_TYPE + Config: + RSABits: "2048" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_BITS + RSAHasher: "sha256" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_HASHER # WebKeys: # Type: "ecdsa" # Config: - # Curve: "P256" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_CURVE + # EllipticCurve: "P256" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_CURVE # Sets the default values for lifetime and expiration for OIDC in each newly created instance # This default can be overwritten for each instance during runtime @@ -1101,7 +1100,25 @@ DefaultInstance: LoginDefaultOrg: true # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINDEFAULTORG # TriggerIntrospectionProjections: false # ZITADEL_DEFAULTINSTANCE_FEATURES_TRIGGERINTROSPECTIONPROJECTIONS # LegacyIntrospection: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LEGACYINTROSPECTION + # UserSchema: false # ZITADEL_DEFAULTINSTANCE_FEATURES_USERSCHEMA + # TokenExchange: false # ZITADEL_DEFAULTINSTANCE_FEATURES_TOKENEXCHANGE + # ImprovedPerformance: # ZITADEL_DEFAULTINSTANCE_FEATURES_IMPROVEDPERFORMANCE + # - OrgByID + # - ProjectGrant + # - Project + # - UserGrant + # - OrgDomainVerified + # WebKey: false # ZITADEL_DEFAULTINSTANCE_FEATURES_WEBKEY + # DebugOIDCParentError: false # ZITADEL_DEFAULTINSTANCE_FEATURES_DEBUGOIDCPARENTERROR + # OIDCSingleV1SessionTermination: false # ZITADEL_DEFAULTINSTANCE_FEATURES_OIDCSINGLEV1SESSIONTERMINATION + # DisableUserTokenEvent: false # ZITADEL_DEFAULTINSTANCE_FEATURES_DISABLEUSERTOKENEVENT + # EnableBackChannelLogout: false # ZITADEL_DEFAULTINSTANCE_FEATURES_ENABLEBACKCHANNELLOGOUT + # LoginV2: + # Required: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED + # BaseURI: "" # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI # PermissionCheckV2: false # ZITADEL_DEFAULTINSTANCE_FEATURES_PERMISSIONCHECKV2 + # ConsoleUseV2UserApi: false # ZITADEL_DEFAULTINSTANCE_FEATURES_CONSOLEUSEV2USERAPI + Limits: # AuditLogRetention limits the number of events that can be queried via the events API by their age. # A value of "0s" means that all events are available. diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go index baa45c6c6e..e146ac2db6 100644 --- a/internal/api/grpc/feature/v2/converter.go +++ b/internal/api/grpc/feature/v2/converter.go @@ -172,7 +172,7 @@ func improvedPerformanceTypesToPb(types []feature.ImprovedPerformanceType) []fea func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb.ImprovedPerformance { switch typ { - case feature.ImprovedPerformanceTypeUnknown: + case feature.ImprovedPerformanceTypeUnspecified: return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED case feature.ImprovedPerformanceTypeOrgByID: return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID @@ -205,7 +205,7 @@ func improvedPerformanceListToDomain(list []feature_pb.ImprovedPerformance) []fe func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.ImprovedPerformanceType { switch typ { case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED: - return feature.ImprovedPerformanceTypeUnknown + return feature.ImprovedPerformanceTypeUnspecified case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID: return feature.ImprovedPerformanceTypeOrgByID case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT: @@ -217,6 +217,6 @@ func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.Imp case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED: return feature.ImprovedPerformanceTypeOrgDomainVerified default: - return feature.ImprovedPerformanceTypeUnknown + return feature.ImprovedPerformanceTypeUnspecified } } diff --git a/internal/api/grpc/feature/v2beta/converter.go b/internal/api/grpc/feature/v2beta/converter.go index bbb375716e..9739e1c4c8 100644 --- a/internal/api/grpc/feature/v2beta/converter.go +++ b/internal/api/grpc/feature/v2beta/converter.go @@ -109,7 +109,7 @@ func improvedPerformanceTypesToPb(types []feature.ImprovedPerformanceType) []fea func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb.ImprovedPerformance { switch typ { - case feature.ImprovedPerformanceTypeUnknown: + case feature.ImprovedPerformanceTypeUnspecified: return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED case feature.ImprovedPerformanceTypeOrgByID: return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID @@ -142,7 +142,7 @@ func improvedPerformanceListToDomain(list []feature_pb.ImprovedPerformance) []fe func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.ImprovedPerformanceType { switch typ { case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED: - return feature.ImprovedPerformanceTypeUnknown + return feature.ImprovedPerformanceTypeUnspecified case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID: return feature.ImprovedPerformanceTypeOrgByID case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT: @@ -154,6 +154,6 @@ func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.Imp case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED: return feature.ImprovedPerformanceTypeOrgDomainVerified default: - return feature.ImprovedPerformanceTypeUnknown + return feature.ImprovedPerformanceTypeUnspecified } } diff --git a/internal/feature/feature.go b/internal/feature/feature.go index 389b750483..f500b80eb3 100644 --- a/internal/feature/feature.go +++ b/internal/feature/feature.go @@ -57,10 +57,11 @@ type Features struct { ConsoleUseV2UserApi bool `json:"console_use_v2_user_api,omitempty"` } +//go:generate enumer -type ImprovedPerformanceType -trimprefix ImprovedPerformanceType -text type ImprovedPerformanceType int32 const ( - ImprovedPerformanceTypeUnknown = iota + ImprovedPerformanceTypeUnspecified ImprovedPerformanceType = iota ImprovedPerformanceTypeOrgByID ImprovedPerformanceTypeProjectGrant ImprovedPerformanceTypeProject diff --git a/internal/feature/improvedperformancetype_enumer.go b/internal/feature/improvedperformancetype_enumer.go new file mode 100644 index 0000000000..a12673c205 --- /dev/null +++ b/internal/feature/improvedperformancetype_enumer.go @@ -0,0 +1,106 @@ +// Code generated by "enumer -type ImprovedPerformanceType -trimprefix ImprovedPerformanceType -text"; DO NOT EDIT. + +package feature + +import ( + "fmt" + "strings" +) + +const _ImprovedPerformanceTypeName = "UnspecifiedOrgByIDProjectGrantProjectUserGrantOrgDomainVerified" + +var _ImprovedPerformanceTypeIndex = [...]uint8{0, 11, 18, 30, 37, 46, 63} + +const _ImprovedPerformanceTypeLowerName = "unspecifiedorgbyidprojectgrantprojectusergrantorgdomainverified" + +func (i ImprovedPerformanceType) String() string { + if i < 0 || i >= ImprovedPerformanceType(len(_ImprovedPerformanceTypeIndex)-1) { + return fmt.Sprintf("ImprovedPerformanceType(%d)", i) + } + return _ImprovedPerformanceTypeName[_ImprovedPerformanceTypeIndex[i]:_ImprovedPerformanceTypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _ImprovedPerformanceTypeNoOp() { + var x [1]struct{} + _ = x[ImprovedPerformanceTypeUnspecified-(0)] + _ = x[ImprovedPerformanceTypeOrgByID-(1)] + _ = x[ImprovedPerformanceTypeProjectGrant-(2)] + _ = x[ImprovedPerformanceTypeProject-(3)] + _ = x[ImprovedPerformanceTypeUserGrant-(4)] + _ = x[ImprovedPerformanceTypeOrgDomainVerified-(5)] +} + +var _ImprovedPerformanceTypeValues = []ImprovedPerformanceType{ImprovedPerformanceTypeUnspecified, ImprovedPerformanceTypeOrgByID, ImprovedPerformanceTypeProjectGrant, ImprovedPerformanceTypeProject, ImprovedPerformanceTypeUserGrant, ImprovedPerformanceTypeOrgDomainVerified} + +var _ImprovedPerformanceTypeNameToValueMap = map[string]ImprovedPerformanceType{ + _ImprovedPerformanceTypeName[0:11]: ImprovedPerformanceTypeUnspecified, + _ImprovedPerformanceTypeLowerName[0:11]: ImprovedPerformanceTypeUnspecified, + _ImprovedPerformanceTypeName[11:18]: ImprovedPerformanceTypeOrgByID, + _ImprovedPerformanceTypeLowerName[11:18]: ImprovedPerformanceTypeOrgByID, + _ImprovedPerformanceTypeName[18:30]: ImprovedPerformanceTypeProjectGrant, + _ImprovedPerformanceTypeLowerName[18:30]: ImprovedPerformanceTypeProjectGrant, + _ImprovedPerformanceTypeName[30:37]: ImprovedPerformanceTypeProject, + _ImprovedPerformanceTypeLowerName[30:37]: ImprovedPerformanceTypeProject, + _ImprovedPerformanceTypeName[37:46]: ImprovedPerformanceTypeUserGrant, + _ImprovedPerformanceTypeLowerName[37:46]: ImprovedPerformanceTypeUserGrant, + _ImprovedPerformanceTypeName[46:63]: ImprovedPerformanceTypeOrgDomainVerified, + _ImprovedPerformanceTypeLowerName[46:63]: ImprovedPerformanceTypeOrgDomainVerified, +} + +var _ImprovedPerformanceTypeNames = []string{ + _ImprovedPerformanceTypeName[0:11], + _ImprovedPerformanceTypeName[11:18], + _ImprovedPerformanceTypeName[18:30], + _ImprovedPerformanceTypeName[30:37], + _ImprovedPerformanceTypeName[37:46], + _ImprovedPerformanceTypeName[46:63], +} + +// ImprovedPerformanceTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func ImprovedPerformanceTypeString(s string) (ImprovedPerformanceType, error) { + if val, ok := _ImprovedPerformanceTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _ImprovedPerformanceTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to ImprovedPerformanceType values", s) +} + +// ImprovedPerformanceTypeValues returns all values of the enum +func ImprovedPerformanceTypeValues() []ImprovedPerformanceType { + return _ImprovedPerformanceTypeValues +} + +// ImprovedPerformanceTypeStrings returns a slice of all String values of the enum +func ImprovedPerformanceTypeStrings() []string { + strs := make([]string, len(_ImprovedPerformanceTypeNames)) + copy(strs, _ImprovedPerformanceTypeNames) + return strs +} + +// IsAImprovedPerformanceType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i ImprovedPerformanceType) IsAImprovedPerformanceType() bool { + for _, v := range _ImprovedPerformanceTypeValues { + if i == v { + return true + } + } + return false +} + +// MarshalText implements the encoding.TextMarshaler interface for ImprovedPerformanceType +func (i ImprovedPerformanceType) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for ImprovedPerformanceType +func (i *ImprovedPerformanceType) UnmarshalText(text []byte) error { + var err error + *i, err = ImprovedPerformanceTypeString(string(text)) + return err +} diff --git a/internal/feature/key_enumer.go b/internal/feature/key_enumer.go index 6466061718..a47b3eb4d9 100644 --- a/internal/feature/key_enumer.go +++ b/internal/feature/key_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" +const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactions_deprecatedimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" -var _KeyIndex = [...]uint16{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163, 197, 221, 247, 255, 274, 297} +var _KeyIndex = [...]uint16{0, 11, 28, 61, 81, 92, 106, 124, 144, 151, 174, 208, 232, 258, 266, 285, 308} -const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" +const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactions_deprecatedimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" func (i Key) String() string { if i < 0 || i >= Key(len(_KeyIndex)-1) { @@ -57,26 +57,26 @@ var _KeyNameToValueMap = map[string]Key{ _KeyLowerName[81:92]: KeyUserSchema, _KeyName[92:106]: KeyTokenExchange, _KeyLowerName[92:106]: KeyTokenExchange, - _KeyName[106:113]: KeyActionsDeprecated, - _KeyLowerName[106:113]: KeyActionsDeprecated, - _KeyName[113:133]: KeyImprovedPerformance, - _KeyLowerName[113:133]: KeyImprovedPerformance, - _KeyName[133:140]: KeyWebKey, - _KeyLowerName[133:140]: KeyWebKey, - _KeyName[140:163]: KeyDebugOIDCParentError, - _KeyLowerName[140:163]: KeyDebugOIDCParentError, - _KeyName[163:197]: KeyOIDCSingleV1SessionTermination, - _KeyLowerName[163:197]: KeyOIDCSingleV1SessionTermination, - _KeyName[197:221]: KeyDisableUserTokenEvent, - _KeyLowerName[197:221]: KeyDisableUserTokenEvent, - _KeyName[221:247]: KeyEnableBackChannelLogout, - _KeyLowerName[221:247]: KeyEnableBackChannelLogout, - _KeyName[247:255]: KeyLoginV2, - _KeyLowerName[247:255]: KeyLoginV2, - _KeyName[255:274]: KeyPermissionCheckV2, - _KeyLowerName[255:274]: KeyPermissionCheckV2, - _KeyName[274:297]: KeyConsoleUseV2UserApi, - _KeyLowerName[274:297]: KeyConsoleUseV2UserApi, + _KeyName[106:124]: KeyActionsDeprecated, + _KeyLowerName[106:124]: KeyActionsDeprecated, + _KeyName[124:144]: KeyImprovedPerformance, + _KeyLowerName[124:144]: KeyImprovedPerformance, + _KeyName[144:151]: KeyWebKey, + _KeyLowerName[144:151]: KeyWebKey, + _KeyName[151:174]: KeyDebugOIDCParentError, + _KeyLowerName[151:174]: KeyDebugOIDCParentError, + _KeyName[174:208]: KeyOIDCSingleV1SessionTermination, + _KeyLowerName[174:208]: KeyOIDCSingleV1SessionTermination, + _KeyName[208:232]: KeyDisableUserTokenEvent, + _KeyLowerName[208:232]: KeyDisableUserTokenEvent, + _KeyName[232:258]: KeyEnableBackChannelLogout, + _KeyLowerName[232:258]: KeyEnableBackChannelLogout, + _KeyName[258:266]: KeyLoginV2, + _KeyLowerName[258:266]: KeyLoginV2, + _KeyName[266:285]: KeyPermissionCheckV2, + _KeyLowerName[266:285]: KeyPermissionCheckV2, + _KeyName[285:308]: KeyConsoleUseV2UserApi, + _KeyLowerName[285:308]: KeyConsoleUseV2UserApi, } var _KeyNames = []string{ @@ -86,16 +86,16 @@ var _KeyNames = []string{ _KeyName[61:81], _KeyName[81:92], _KeyName[92:106], - _KeyName[106:113], - _KeyName[113:133], - _KeyName[133:140], - _KeyName[140:163], - _KeyName[163:197], - _KeyName[197:221], - _KeyName[221:247], - _KeyName[247:255], - _KeyName[255:274], - _KeyName[274:297], + _KeyName[106:124], + _KeyName[124:144], + _KeyName[144:151], + _KeyName[151:174], + _KeyName[174:208], + _KeyName[208:232], + _KeyName[232:258], + _KeyName[258:266], + _KeyName[266:285], + _KeyName[285:308], } // KeyString retrieves an enum value from the enum constants string name. From 181186e477f7ae1779cf2b4429f25e00b9712e98 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:29:16 +0200 Subject: [PATCH 15/26] fix(mirror): add max auth request age configuration (#9812) # Which Problems Are Solved The `auth.auth_requests` table is not cleaned up so long running Zitadel installations can contain many rows. The mirror command can take long because a the data are first copied into memory (or disk) on cockroach and users do not get any output from mirror. This is unfortunate because people don't know if Zitadel got stuck. # How the Problems Are Solved Enhance logging throughout the projection processes and introduce a configuration option for the maximum age of authentication requests. # Additional Changes None # Additional Context closes https://github.com/zitadel/zitadel/issues/9764 --------- Co-authored-by: Livio Spring --- cmd/mirror/auth.go | 15 ++- cmd/mirror/config.go | 3 +- cmd/mirror/defaults.yaml | 97 ++++++++++--------- cmd/mirror/event_store.go | 5 + cmd/mirror/projections.go | 10 +- cmd/mirror/system.go | 14 +-- docs/docs/self-hosting/manage/cli/mirror.mdx | 3 + .../eventsourcing/handler/handler.go | 8 +- .../eventsourcing/handler/handler.go | 8 +- internal/notification/projections.go | 8 +- internal/query/projection/projection.go | 12 ++- 11 files changed, 116 insertions(+), 67 deletions(-) diff --git a/cmd/mirror/auth.go b/cmd/mirror/auth.go index 0eba10d05f..3d7ae45bce 100644 --- a/cmd/mirror/auth.go +++ b/cmd/mirror/auth.go @@ -4,6 +4,7 @@ import ( "context" _ "embed" "io" + "strconv" "time" "github.com/jackc/pgx/v5/stdlib" @@ -41,12 +42,16 @@ func copyAuth(ctx context.Context, config *Migration) { logging.OnError(err).Fatal("unable to connect to destination database") defer destClient.Close() - copyAuthRequests(ctx, sourceClient, destClient) + copyAuthRequests(ctx, sourceClient, destClient, config.MaxAuthRequestAge) } -func copyAuthRequests(ctx context.Context, source, dest *database.DB) { +func copyAuthRequests(ctx context.Context, source, dest *database.DB, maxAuthRequestAge time.Duration) { start := time.Now() + logging.Info("creating index on auth.auth_requests.change_date to speed up copy in source database") + _, err := source.ExecContext(ctx, "CREATE INDEX CONCURRENTLY IF NOT EXISTS auth_requests_change_date ON auth.auth_requests (change_date)") + logging.OnError(err).Fatal("unable to create index on auth.auth_requests.change_date") + sourceConn, err := source.Conn(ctx) logging.OnError(err).Fatal("unable to acquire connection") defer sourceConn.Close() @@ -55,9 +60,9 @@ func copyAuthRequests(ctx context.Context, source, dest *database.DB) { errs := make(chan error, 1) go func() { - err = sourceConn.Raw(func(driverConn interface{}) error { + err = sourceConn.Raw(func(driverConn any) error { conn := driverConn.(*stdlib.Conn).Conn() - _, err := conn.PgConn().CopyTo(ctx, w, "COPY (SELECT id, regexp_replace(request::TEXT, '\\\\u0000', '', 'g')::JSON request, code, request_type, creation_date, change_date, instance_id FROM auth.auth_requests "+instanceClause()+") TO STDOUT") + _, err := conn.PgConn().CopyTo(ctx, w, "COPY (SELECT id, regexp_replace(request::TEXT, '\\\\u0000', '', 'g')::JSON request, code, request_type, creation_date, change_date, instance_id FROM auth.auth_requests "+instanceClause()+" AND change_date > NOW() - INTERVAL '"+strconv.FormatFloat(maxAuthRequestAge.Seconds(), 'f', -1, 64)+" seconds') TO STDOUT") w.Close() return err }) @@ -69,7 +74,7 @@ func copyAuthRequests(ctx context.Context, source, dest *database.DB) { defer destConn.Close() var affected int64 - err = destConn.Raw(func(driverConn interface{}) error { + err = destConn.Raw(func(driverConn any) error { conn := driverConn.(*stdlib.Conn).Conn() if shouldReplace { diff --git a/cmd/mirror/config.go b/cmd/mirror/config.go index 9d0113a1d7..5bb19f12de 100644 --- a/cmd/mirror/config.go +++ b/cmd/mirror/config.go @@ -23,7 +23,8 @@ type Migration struct { Source database.Config Destination database.Config - EventBulkSize uint32 + EventBulkSize uint32 + MaxAuthRequestAge time.Duration Log *logging.Config Machine *id.Config diff --git a/cmd/mirror/defaults.yaml b/cmd/mirror/defaults.yaml index 4b42c06534..4d8a0a4eae 100644 --- a/cmd/mirror/defaults.yaml +++ b/cmd/mirror/defaults.yaml @@ -1,61 +1,64 @@ Source: cockroach: - Host: localhost # ZITADEL_DATABASE_COCKROACH_HOST - Port: 26257 # ZITADEL_DATABASE_COCKROACH_PORT - Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE - MaxOpenConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS - MaxIdleConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS - MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME - MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME - Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS + Host: localhost # ZITADEL_SOURCE_COCKROACH_HOST + Port: 26257 # ZITADEL_SOURCE_COCKROACH_PORT + Database: zitadel # ZITADEL_SOURCE_COCKROACH_DATABASE + MaxOpenConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXOPENCONNS + MaxIdleConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXIDLECONNS + MaxConnLifetime: 30m # ZITADEL_SOURCE_COCKROACH_MAXCONNLIFETIME + MaxConnIdleTime: 5m # ZITADEL_SOURCE_COCKROACH_MAXCONNIDLETIME + Options: "" # ZITADEL_SOURCE_COCKROACH_OPTIONS User: - Username: zitadel # ZITADEL_DATABASE_COCKROACH_USER_USERNAME - Password: "" # ZITADEL_DATABASE_COCKROACH_USER_PASSWORD + Username: zitadel # ZITADEL_SOURCE_COCKROACH_USER_USERNAME + Password: "" # ZITADEL_SOURCE_COCKROACH_USER_PASSWORD SSL: - Mode: disable # ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE - RootCert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT - Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT - Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY + Mode: disable # ZITADEL_SOURCE_COCKROACH_USER_SSL_MODE + RootCert: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_ROOTCERT + Cert: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_CERT + Key: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_KEY # Postgres is used as soon as a value is set # The values describe the possible fields to set values postgres: - Host: # ZITADEL_DATABASE_POSTGRES_HOST - Port: # ZITADEL_DATABASE_POSTGRES_PORT - Database: # ZITADEL_DATABASE_POSTGRES_DATABASE - MaxOpenConns: # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS - MaxIdleConns: # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS - MaxConnLifetime: # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME - MaxConnIdleTime: # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME - Options: # ZITADEL_DATABASE_POSTGRES_OPTIONS + Host: # ZITADEL_SOURCE_POSTGRES_HOST + Port: # ZITADEL_SOURCE_POSTGRES_PORT + Database: # ZITADEL_SOURCE_POSTGRES_DATABASE + MaxOpenConns: # ZITADEL_SOURCE_POSTGRES_MAXOPENCONNS + MaxIdleConns: # ZITADEL_SOURCE_POSTGRES_MAXIDLECONNS + MaxConnLifetime: # ZITADEL_SOURCE_POSTGRES_MAXCONNLIFETIME + MaxConnIdleTime: # ZITADEL_SOURCE_POSTGRES_MAXCONNIDLETIME + Options: # ZITADEL_SOURCE_POSTGRES_OPTIONS User: - Username: # ZITADEL_DATABASE_POSTGRES_USER_USERNAME - Password: # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD + Username: # ZITADEL_SOURCE_POSTGRES_USER_USERNAME + Password: # ZITADEL_SOURCE_POSTGRES_USER_PASSWORD SSL: - Mode: # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE - RootCert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT - Cert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT - Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY + Mode: # ZITADEL_SOURCE_POSTGRES_USER_SSL_MODE + RootCert: # ZITADEL_SOURCE_POSTGRES_USER_SSL_ROOTCERT + Cert: # ZITADEL_SOURCE_POSTGRES_USER_SSL_CERT + Key: # ZITADEL_SOURCE_POSTGRES_USER_SSL_KEY Destination: postgres: - Host: localhost # ZITADEL_DATABASE_POSTGRES_HOST - Port: 5432 # ZITADEL_DATABASE_POSTGRES_PORT - Database: zitadel # ZITADEL_DATABASE_POSTGRES_DATABASE - MaxOpenConns: 5 # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS - MaxIdleConns: 2 # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS - MaxConnLifetime: 30m # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME - MaxConnIdleTime: 5m # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME - Options: "" # ZITADEL_DATABASE_POSTGRES_OPTIONS + Host: localhost # ZITADEL_DESTINATION_POSTGRES_HOST + Port: 5432 # ZITADEL_DESTINATION_POSTGRES_PORT + Database: zitadel # ZITADEL_DESTINATION_POSTGRES_DATABASE + MaxOpenConns: 5 # ZITADEL_DESTINATION_POSTGRES_MAXOPENCONNS + MaxIdleConns: 2 # ZITADEL_DESTINATION_POSTGRES_MAXIDLECONNS + MaxConnLifetime: 30m # ZITADEL_DESTINATION_POSTGRES_MAXCONNLIFETIME + MaxConnIdleTime: 5m # ZITADEL_DESTINATION_POSTGRES_MAXCONNIDLETIME + Options: "" # ZITADEL_DESTINATION_POSTGRES_OPTIONS User: - Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME - Password: "" # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD + Username: zitadel # ZITADEL_DESTINATION_POSTGRES_USER_USERNAME + Password: "" # ZITADEL_DESTINATION_POSTGRES_USER_PASSWORD SSL: - Mode: disable # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE - RootCert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT - Cert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT - Key: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY + Mode: disable # ZITADEL_DESTINATION_POSTGRES_USER_SSL_MODE + RootCert: "" # ZITADEL_DESTINATION_POSTGRES_USER_SSL_ROOTCERT + Cert: "" # ZITADEL_DESTINATION_POSTGRES_USER_SSL_CERT + Key: "" # ZITADEL_DESTINATION_POSTGRES_USER_SSL_KEY -EventBulkSize: 10000 +EventBulkSize: 10000 # ZITADEL_EVENTBULKSIZE +# The maximum duration an auth request was last updated before it gets ignored. +# Default is 30 days +MaxAuthRequestAge: 720h # ZITADEL_MAXAUTHREQUESTAGE Projections: # The maximum duration a transaction remains open @@ -64,14 +67,14 @@ Projections: TransactionDuration: 0s # ZITADEL_PROJECTIONS_TRANSACTIONDURATION # turn off scheduler during operation RequeueEvery: 0s - ConcurrentInstances: 7 - EventBulkLimit: 1000 - Customizations: + ConcurrentInstances: 7 # ZITADEL_PROJECTIONS_CONCURRENTINSTANCES + EventBulkLimit: 1000 # ZITADEL_PROJECTIONS_EVENTBULKLIMIT + Customizations: notifications: MaxFailureCount: 1 Eventstore: - MaxRetries: 3 + MaxRetries: 3 # ZITADEL_EVENTSTORE_MAXRETRIES Auth: Spooler: diff --git a/cmd/mirror/event_store.go b/cmd/mirror/event_store.go index 8ce53b150a..41c529c025 100644 --- a/cmd/mirror/event_store.go +++ b/cmd/mirror/event_store.go @@ -69,6 +69,7 @@ func positionQuery(db *db.DB) string { } func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { + logging.Info("starting to copy events") start := time.Now() reader, writer := io.Pipe() @@ -130,7 +131,10 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { if err != nil { return zerrors.ThrowUnknownf(err, "MIGRA-KTuSq", "unable to copy events from source during iteration %d", i) } + logging.WithFields("batch_count", i).Info("batch of events copied") + if tag.RowsAffected() < int64(bulkSize) { + logging.WithFields("batch_count", i).Info("last batch of events copied") return nil } @@ -202,6 +206,7 @@ func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, sou } func copyUniqueConstraints(ctx context.Context, source, dest *db.DB) { + logging.Info("starting to copy unique constraints") start := time.Now() reader, writer := io.Pipe() errs := make(chan error, 1) diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index 66b3fb1a26..4e12b29748 100644 --- a/cmd/mirror/projections.go +++ b/cmd/mirror/projections.go @@ -3,6 +3,7 @@ package mirror import ( "context" "database/sql" + "fmt" "net/http" "sync" "time" @@ -104,6 +105,7 @@ func projections( config *ProjectionsConfig, masterKey string, ) { + logging.Info("starting to fill projections") start := time.Now() client, err := database.Connect(config.Destination, false) @@ -255,8 +257,10 @@ func projections( go execProjections(ctx, instances, failedInstances, &wg) } - for _, instance := range queryInstanceIDs(ctx, client) { + existingInstances := queryInstanceIDs(ctx, client) + for i, instance := range existingInstances { instances <- instance + logging.WithFields("id", instance, "index", fmt.Sprintf("%d/%d", i, len(existingInstances))).Info("instance queued for projection") } close(instances) wg.Wait() @@ -268,7 +272,7 @@ func projections( func execProjections(ctx context.Context, instances <-chan string, failedInstances chan<- string, wg *sync.WaitGroup) { for instance := range instances { - logging.WithFields("instance", instance).Info("start projections") + logging.WithFields("instance", instance).Info("starting projections") ctx = internal_authz.WithInstanceID(ctx, instance) err := projection.ProjectInstance(ctx) @@ -311,7 +315,7 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc wg.Done() } -// returns the instance configured by flag +// queryInstanceIDs returns the instance configured by flag // or all instances which are not removed func queryInstanceIDs(ctx context.Context, source *database.DB) []string { if len(instanceIDs) > 0 { diff --git a/cmd/mirror/system.go b/cmd/mirror/system.go index 00b48eb491..57eb205436 100644 --- a/cmd/mirror/system.go +++ b/cmd/mirror/system.go @@ -46,6 +46,7 @@ func copySystem(ctx context.Context, config *Migration) { } func copyAssets(ctx context.Context, source, dest *database.DB) { + logging.Info("starting to copy assets") start := time.Now() sourceConn, err := source.Conn(ctx) @@ -70,7 +71,7 @@ func copyAssets(ctx context.Context, source, dest *database.DB) { logging.OnError(err).Fatal("unable to acquire dest connection") defer destConn.Close() - var eventCount int64 + var assetCount int64 err = destConn.Raw(func(driverConn interface{}) error { conn := driverConn.(*stdlib.Conn).Conn() @@ -82,16 +83,17 @@ func copyAssets(ctx context.Context, source, dest *database.DB) { } tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.assets (instance_id, asset_type, resource_owner, name, content_type, data, updated_at) FROM stdin") - eventCount = tag.RowsAffected() + assetCount = tag.RowsAffected() return err }) logging.OnError(err).Fatal("unable to copy assets to destination") logging.OnError(<-errs).Fatal("unable to copy assets from source") - logging.WithFields("took", time.Since(start), "count", eventCount).Info("assets migrated") + logging.WithFields("took", time.Since(start), "count", assetCount).Info("assets migrated") } func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) { + logging.Info("starting to copy encryption keys") start := time.Now() sourceConn, err := source.Conn(ctx) @@ -116,7 +118,7 @@ func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) { logging.OnError(err).Fatal("unable to acquire dest connection") defer destConn.Close() - var eventCount int64 + var keyCount int64 err = destConn.Raw(func(driverConn interface{}) error { conn := driverConn.(*stdlib.Conn).Conn() @@ -128,11 +130,11 @@ func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) { } tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.encryption_keys FROM stdin") - eventCount = tag.RowsAffected() + keyCount = tag.RowsAffected() return err }) logging.OnError(err).Fatal("unable to copy encryption keys to destination") logging.OnError(<-errs).Fatal("unable to copy encryption keys from source") - logging.WithFields("took", time.Since(start), "count", eventCount).Info("encryption keys migrated") + logging.WithFields("took", time.Since(start), "count", keyCount).Info("encryption keys migrated") } diff --git a/docs/docs/self-hosting/manage/cli/mirror.mdx b/docs/docs/self-hosting/manage/cli/mirror.mdx index 45bac9b279..ae81800e39 100644 --- a/docs/docs/self-hosting/manage/cli/mirror.mdx +++ b/docs/docs/self-hosting/manage/cli/mirror.mdx @@ -158,6 +158,9 @@ Destination: # As cockroachdb first copies the data into memory this parameter is used to iterate through the events table and fetch only the given amount of events per iteration EventBulkSize: 10000 # ZITADEL_EVENTBULKSIZE +# The maximum duration an auth request was last updated before it gets ignored. +# Default is 30 days +MaxAuthRequestAge: 720h # ZITADEL_MAXAUTHREQUESTAGE Projections: # Defines how many projections are allowed to run in parallel diff --git a/internal/admin/repository/eventsourcing/handler/handler.go b/internal/admin/repository/eventsourcing/handler/handler.go index ec268c25a1..76584b55b0 100644 --- a/internal/admin/repository/eventsourcing/handler/handler.go +++ b/internal/admin/repository/eventsourcing/handler/handler.go @@ -2,9 +2,13 @@ package handler import ( "context" + "fmt" "time" + "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -57,11 +61,13 @@ func Start(ctx context.Context) { } func ProjectInstance(ctx context.Context) error { - for _, projection := range projections { + for i, projection := range projections { + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting admin projection") _, err := projection.Trigger(ctx) if err != nil { return err } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("admin projection done") } return nil } diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go index 0d87ab06bb..74a27a8312 100644 --- a/internal/auth/repository/eventsourcing/handler/handler.go +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -2,8 +2,12 @@ package handler import ( "context" + "fmt" "time" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" @@ -72,11 +76,13 @@ func Projections() []*handler2.Handler { } func ProjectInstance(ctx context.Context) error { - for _, projection := range projections { + for i, projection := range projections { + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting auth projection") _, err := projection.Trigger(ctx) if err != nil { return err } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("auth projection done") } return nil } diff --git a/internal/notification/projections.go b/internal/notification/projections.go index a2d4d4140e..9b6b975fa1 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -2,8 +2,12 @@ package notification import ( "context" + "fmt" "time" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" @@ -68,11 +72,13 @@ func Start(ctx context.Context) { } func ProjectInstance(ctx context.Context) error { - for _, projection := range projections { + for i, projection := range projections { + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting notification projection") _, err := projection.Trigger(ctx) if err != nil { return err } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("notification projection done") } return nil } diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index f4e3bbe0d4..07953a27e8 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -2,6 +2,9 @@ package projection import ( "context" + "fmt" + + "github.com/zitadel/logging" internal_authz "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" @@ -90,6 +93,7 @@ var ( ) type projection interface { + ProjectionName() string Start(ctx context.Context) Init(ctx context.Context) error Trigger(ctx context.Context, opts ...handler.TriggerOpt) (_ context.Context, err error) @@ -206,21 +210,25 @@ func Start(ctx context.Context) { } func ProjectInstance(ctx context.Context) error { - for _, projection := range projections { + for i, projection := range projections { + logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting projection") _, err := projection.Trigger(ctx) if err != nil { return err } + logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).Info("projection done") } return nil } func ProjectInstanceFields(ctx context.Context) error { - for _, fieldProjection := range fields { + for i, fieldProjection := range fields { + logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(fields))).Info("starting fields projection") err := fieldProjection.Trigger(ctx) if err != nil { return err } + logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).Info("fields projection done") } return nil } From 0465d5093ef009e9bbea6998ca383bcb20144136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 30 Apr 2025 10:26:04 +0200 Subject: [PATCH 16/26] fix(features): remove the improved performance enumer (#9819) # Which Problems Are Solved Instance that had improved performance flags set, got event errors when getting instance features. This is because the improved performance flags were marshalled using the enumerated integers, but now needed to be unmashalled using the added UnmarshallText method. # How the Problems Are Solved - Remove emnumer generation # Additional Changes - none # Additional Context - reported on QA - Backport to next-rc / v3 --- cmd/defaults.yaml | 13 ++- internal/feature/feature.go | 3 +- .../feature/improvedperformancetype_enumer.go | 106 ------------------ 3 files changed, 9 insertions(+), 113 deletions(-) delete mode 100644 internal/feature/improvedperformancetype_enumer.go diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index f20fbc03fc..6ab01ab35b 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -1102,12 +1102,13 @@ DefaultInstance: # LegacyIntrospection: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LEGACYINTROSPECTION # UserSchema: false # ZITADEL_DEFAULTINSTANCE_FEATURES_USERSCHEMA # TokenExchange: false # ZITADEL_DEFAULTINSTANCE_FEATURES_TOKENEXCHANGE - # ImprovedPerformance: # ZITADEL_DEFAULTINSTANCE_FEATURES_IMPROVEDPERFORMANCE - # - OrgByID - # - ProjectGrant - # - Project - # - UserGrant - # - OrgDomainVerified + ImprovedPerformance: # ZITADEL_DEFAULTINSTANCE_FEATURES_IMPROVEDPERFORMANCE + # https://github.com/zitadel/zitadel/blob/main/internal/feature/feature.go#L64-L68 + # - 1 # OrgByID + # - 2 # ProjectGrant + # - 3 # Project + # - 4 # UserGrant + # - 5 # OrgDomainVerified # WebKey: false # ZITADEL_DEFAULTINSTANCE_FEATURES_WEBKEY # DebugOIDCParentError: false # ZITADEL_DEFAULTINSTANCE_FEATURES_DEBUGOIDCPARENTERROR # OIDCSingleV1SessionTermination: false # ZITADEL_DEFAULTINSTANCE_FEATURES_OIDCSINGLEV1SESSIONTERMINATION diff --git a/internal/feature/feature.go b/internal/feature/feature.go index f500b80eb3..b5f5a901d4 100644 --- a/internal/feature/feature.go +++ b/internal/feature/feature.go @@ -57,7 +57,8 @@ type Features struct { ConsoleUseV2UserApi bool `json:"console_use_v2_user_api,omitempty"` } -//go:generate enumer -type ImprovedPerformanceType -trimprefix ImprovedPerformanceType -text +/* Note: do not generate the stringer or enumer for this type, is it breaks existing events */ + type ImprovedPerformanceType int32 const ( diff --git a/internal/feature/improvedperformancetype_enumer.go b/internal/feature/improvedperformancetype_enumer.go deleted file mode 100644 index a12673c205..0000000000 --- a/internal/feature/improvedperformancetype_enumer.go +++ /dev/null @@ -1,106 +0,0 @@ -// Code generated by "enumer -type ImprovedPerformanceType -trimprefix ImprovedPerformanceType -text"; DO NOT EDIT. - -package feature - -import ( - "fmt" - "strings" -) - -const _ImprovedPerformanceTypeName = "UnspecifiedOrgByIDProjectGrantProjectUserGrantOrgDomainVerified" - -var _ImprovedPerformanceTypeIndex = [...]uint8{0, 11, 18, 30, 37, 46, 63} - -const _ImprovedPerformanceTypeLowerName = "unspecifiedorgbyidprojectgrantprojectusergrantorgdomainverified" - -func (i ImprovedPerformanceType) String() string { - if i < 0 || i >= ImprovedPerformanceType(len(_ImprovedPerformanceTypeIndex)-1) { - return fmt.Sprintf("ImprovedPerformanceType(%d)", i) - } - return _ImprovedPerformanceTypeName[_ImprovedPerformanceTypeIndex[i]:_ImprovedPerformanceTypeIndex[i+1]] -} - -// An "invalid array index" compiler error signifies that the constant values have changed. -// Re-run the stringer command to generate them again. -func _ImprovedPerformanceTypeNoOp() { - var x [1]struct{} - _ = x[ImprovedPerformanceTypeUnspecified-(0)] - _ = x[ImprovedPerformanceTypeOrgByID-(1)] - _ = x[ImprovedPerformanceTypeProjectGrant-(2)] - _ = x[ImprovedPerformanceTypeProject-(3)] - _ = x[ImprovedPerformanceTypeUserGrant-(4)] - _ = x[ImprovedPerformanceTypeOrgDomainVerified-(5)] -} - -var _ImprovedPerformanceTypeValues = []ImprovedPerformanceType{ImprovedPerformanceTypeUnspecified, ImprovedPerformanceTypeOrgByID, ImprovedPerformanceTypeProjectGrant, ImprovedPerformanceTypeProject, ImprovedPerformanceTypeUserGrant, ImprovedPerformanceTypeOrgDomainVerified} - -var _ImprovedPerformanceTypeNameToValueMap = map[string]ImprovedPerformanceType{ - _ImprovedPerformanceTypeName[0:11]: ImprovedPerformanceTypeUnspecified, - _ImprovedPerformanceTypeLowerName[0:11]: ImprovedPerformanceTypeUnspecified, - _ImprovedPerformanceTypeName[11:18]: ImprovedPerformanceTypeOrgByID, - _ImprovedPerformanceTypeLowerName[11:18]: ImprovedPerformanceTypeOrgByID, - _ImprovedPerformanceTypeName[18:30]: ImprovedPerformanceTypeProjectGrant, - _ImprovedPerformanceTypeLowerName[18:30]: ImprovedPerformanceTypeProjectGrant, - _ImprovedPerformanceTypeName[30:37]: ImprovedPerformanceTypeProject, - _ImprovedPerformanceTypeLowerName[30:37]: ImprovedPerformanceTypeProject, - _ImprovedPerformanceTypeName[37:46]: ImprovedPerformanceTypeUserGrant, - _ImprovedPerformanceTypeLowerName[37:46]: ImprovedPerformanceTypeUserGrant, - _ImprovedPerformanceTypeName[46:63]: ImprovedPerformanceTypeOrgDomainVerified, - _ImprovedPerformanceTypeLowerName[46:63]: ImprovedPerformanceTypeOrgDomainVerified, -} - -var _ImprovedPerformanceTypeNames = []string{ - _ImprovedPerformanceTypeName[0:11], - _ImprovedPerformanceTypeName[11:18], - _ImprovedPerformanceTypeName[18:30], - _ImprovedPerformanceTypeName[30:37], - _ImprovedPerformanceTypeName[37:46], - _ImprovedPerformanceTypeName[46:63], -} - -// ImprovedPerformanceTypeString retrieves an enum value from the enum constants string name. -// Throws an error if the param is not part of the enum. -func ImprovedPerformanceTypeString(s string) (ImprovedPerformanceType, error) { - if val, ok := _ImprovedPerformanceTypeNameToValueMap[s]; ok { - return val, nil - } - - if val, ok := _ImprovedPerformanceTypeNameToValueMap[strings.ToLower(s)]; ok { - return val, nil - } - return 0, fmt.Errorf("%s does not belong to ImprovedPerformanceType values", s) -} - -// ImprovedPerformanceTypeValues returns all values of the enum -func ImprovedPerformanceTypeValues() []ImprovedPerformanceType { - return _ImprovedPerformanceTypeValues -} - -// ImprovedPerformanceTypeStrings returns a slice of all String values of the enum -func ImprovedPerformanceTypeStrings() []string { - strs := make([]string, len(_ImprovedPerformanceTypeNames)) - copy(strs, _ImprovedPerformanceTypeNames) - return strs -} - -// IsAImprovedPerformanceType returns "true" if the value is listed in the enum definition. "false" otherwise -func (i ImprovedPerformanceType) IsAImprovedPerformanceType() bool { - for _, v := range _ImprovedPerformanceTypeValues { - if i == v { - return true - } - } - return false -} - -// MarshalText implements the encoding.TextMarshaler interface for ImprovedPerformanceType -func (i ImprovedPerformanceType) MarshalText() ([]byte, error) { - return []byte(i.String()), nil -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface for ImprovedPerformanceType -func (i *ImprovedPerformanceType) UnmarshalText(text []byte) error { - var err error - *i, err = ImprovedPerformanceTypeString(string(text)) - return err -} From 3953879fe9c2533289dab8a67a5e1a0514500150 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:12:48 +0200 Subject: [PATCH 17/26] fix: correct unmarshalling of IdP user when using Google (#9799) # Which Problems Are Solved Users from Google IDP's are not unmarshalled correctly in intent endpoints and not returned to callers. # How the Problems Are Solved Provided correct type for unmarshalling of the information. # Additional Changes None # Additional Context None --------- Co-authored-by: Livio Spring --- internal/api/grpc/user/v2/intent.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go index 6e46dfd5c3..06966edb35 100644 --- a/internal/api/grpc/user/v2/intent.go +++ b/internal/api/grpc/user/v2/intent.go @@ -182,7 +182,7 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R case *gitlab.Provider: idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}) case *google.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}) + idpUser, err = unmarshalIdpUser(intent.IDPUser, &google.User{User: &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}}) case *saml.Provider: idpUser, err = unmarshalIdpUser(intent.IDPUser, &saml.UserMapper{}) case *ldap.Provider: From 002c3eb025693b51b99670d05cd50953b4c8ca48 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 30 Apr 2025 13:16:44 +0200 Subject: [PATCH 18/26] fix: Use ID ordering for the executions in Actions v2 (#9820) # Which Problems Are Solved Sort Executions by ID in the Actions V2 view. This way All is the first element in the table. # How the Problems Are Solved Pass ID sorting to the Backend. # Additional Changes Cleaned up some imports. # Additional Context - Part of Make actions sortable by hirarchie #9688 --- .../actions-two-actions/actions-two-actions.component.ts | 3 ++- console/src/app/services/grpc.service.ts | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) 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 index b5f2260e34..7e0d457dd5 100644 --- 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 @@ -16,6 +16,7 @@ import { MessageInitShape } from '@bufbuild/protobuf'; import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb'; import { InfoSectionType } from '../../info-section/info-section.component'; +import { ExecutionFieldName } from '@zitadel/proto/zitadel/action/v2beta/query_pb'; @Component({ selector: 'cnsl-actions-two-actions', @@ -42,7 +43,7 @@ export class ActionsTwoActionsComponent { return this.refresh$.pipe( startWith(true), switchMap(() => { - return this.actionService.listExecutions({}); + return this.actionService.listExecutions({ sortingColumn: ExecutionFieldName.ID, pagination: { asc: true } }); }), map(({ result }) => result.map(correctlyTypeExecution)), catchError((err) => { diff --git a/console/src/app/services/grpc.service.ts b/console/src/app/services/grpc.service.ts index b2f89ca648..d2add12f41 100644 --- a/console/src/app/services/grpc.service.ts +++ b/console/src/app/services/grpc.service.ts @@ -15,7 +15,6 @@ import { AuthInterceptor, AuthInterceptorProvider, NewConnectWebAuthInterceptor import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.interceptor'; import { I18nInterceptor } from './interceptors/i18n.interceptor'; import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } from './interceptors/org.interceptor'; -import { StorageService } from './storage.service'; import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb'; //@ts-ignore import { createFeatureServiceClient, createUserServiceClient, createSessionServiceClient } from '@zitadel/client/v2'; @@ -24,14 +23,10 @@ import { createAuthServiceClient, createManagementServiceClient } from '@zitadel 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); From 48c1f7e49f47e507e4c31cfc84f8a3a043278969 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 30 Apr 2025 14:22:27 +0200 Subject: [PATCH 19/26] fix: Actions V2 improve deleted target handling in executions (#9822) # Which Problems Are Solved Previously, if a target was deleted but still referenced by an execution, it became impossible to load the executions. # How the Problems Are Solved Missing targets in the execution table are now gracefully ignored, allowing executions to load without errors. # Additional Changes Enhanced permission handling in the settings sidenav to ensure users have the correct access rights. --- .../actions-two-actions-table.component.html | 4 ++-- .../actions-two-actions-table.component.ts | 10 +++------- console/src/app/modules/settings-list/settings.ts | 6 ++---- console/src/app/services/grpc-auth.service.ts | 15 +++++++++++++-- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html index 82f04fb124..7948ba7554 100644 --- a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html @@ -24,8 +24,8 @@
{{ 'ACTIONSTWO.EXECUTION.TABLE.TARGET' | translate }}
- {{ target.name }} + + {{ target.name }}